1 /* 2 * AccessibilityPreferencesPane.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.aria.client.Id; 18 import com.google.gwt.aria.client.Roles; 19 import com.google.gwt.core.client.JsArrayString; 20 import com.google.gwt.dom.client.Style; 21 import com.google.gwt.dom.client.Style.Unit; 22 import com.google.gwt.resources.client.ImageResource; 23 import com.google.gwt.user.client.ui.CheckBox; 24 import com.google.gwt.user.client.ui.Label; 25 import com.google.inject.Inject; 26 import org.rstudio.core.client.ElementIds; 27 import org.rstudio.core.client.StringUtil; 28 import org.rstudio.core.client.dom.DomUtils; 29 import org.rstudio.core.client.prefs.RestartRequirement; 30 import org.rstudio.core.client.resources.ImageResource2x; 31 import org.rstudio.core.client.theme.DialogTabLayoutPanel; 32 import org.rstudio.core.client.theme.VerticalTabPanel; 33 import org.rstudio.core.client.widget.CheckBoxList; 34 import org.rstudio.core.client.widget.NumericValueWidget; 35 import org.rstudio.studio.client.application.AriaLiveService; 36 import org.rstudio.studio.client.common.HelpLink; 37 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 38 39 import java.util.Map; 40 41 public class AccessibilityPreferencesPane extends PreferencesPane 42 { 43 @Inject AccessibilityPreferencesPane(UserPrefs prefs, AriaLiveService ariaLive, PreferencesDialogResources res)44 public AccessibilityPreferencesPane(UserPrefs prefs, 45 AriaLiveService ariaLive, 46 PreferencesDialogResources res) 47 { 48 res_ = res; 49 ariaLive_ = ariaLive; 50 51 VerticalTabPanel generalPanel = new VerticalTabPanel(ElementIds.A11Y_GENERAL_PREFS); 52 VerticalTabPanel announcementsPanel = new VerticalTabPanel(ElementIds.A11Y_ANNOUNCEMENTS_PREFS); 53 54 generalPanel.add(headerLabel("Assistive Tools")); 55 chkScreenReaderEnabled_ = new CheckBox("Screen reader support (requires restart)"); 56 generalPanel.add(chkScreenReaderEnabled_); 57 58 typingStatusDelay_ = numericPref("Milliseconds after typing before speaking results", 59 1, 9999, prefs.typingStatusDelayMs()); 60 generalPanel.add(indent(typingStatusDelay_)); 61 generalPanel.add(indent(maxOutput_ = numericPref("Maximum number of console output lines to read", 62 0, UserPrefs.MAX_SCREEN_READER_CONSOLE_OUTPUT, prefs.screenreaderConsoleAnnounceLimit()))); 63 64 Label displayLabel = headerLabel("Other"); 65 generalPanel.add(displayLabel); 66 displayLabel.getElement().getStyle().setMarginTop(8, Style.Unit.PX); 67 generalPanel.add(checkboxPref("Reduce user interface animations", prefs.reducedMotion())); 68 chkTabMovesFocus_ = new CheckBox("Tab key always moves focus"); 69 generalPanel.add(lessSpaced(chkTabMovesFocus_)); 70 chkShowFocusRectangles_ = new CheckBox("Always show focus outlines (requires restart)"); 71 generalPanel.add(lessSpaced(chkShowFocusRectangles_)); 72 generalPanel.add(checkboxPref("Highlight focused panel", prefs.showPanelFocusRectangle())); 73 74 HelpLink helpLink = new HelpLink("RStudio accessibility help", "rstudio_a11y", false); 75 nudgeRight(helpLink); 76 helpLink.addStyleName(res_.styles().newSection()); 77 generalPanel.add(helpLink); 78 79 Label announcementsLabel = headerLabel("Enable / Disable Announcements"); 80 announcements_ = new CheckBoxList(announcementsLabel); 81 announcementsPanel.add(announcementsLabel); 82 announcementsLabel.getElement().getStyle().setMarginTop(8, Unit.PX); 83 announcementsPanel.add(announcements_); 84 DomUtils.ensureHasId(announcementsLabel.getElement()); 85 Roles.getListboxRole().setAriaLabelledbyProperty(announcements_.getElement(), 86 Id.of(announcementsLabel.getElement())); 87 announcements_.setHeight("380px"); 88 announcements_.setWidth("390px"); 89 announcements_.getElement().getStyle().setMarginBottom(15, Unit.PX); 90 announcements_.getElement().getStyle().setMarginLeft(3, Unit.PX); 91 92 DialogTabLayoutPanel tabPanel = new DialogTabLayoutPanel("Accessibility"); 93 tabPanel.setSize("435px", "533px"); 94 tabPanel.add(generalPanel, "General", generalPanel.getBasePanelId()); 95 tabPanel.add(announcementsPanel, "Announcements", announcementsPanel.getBasePanelId()); 96 tabPanel.selectTab(0); 97 add(tabPanel); 98 } 99 100 @Override getIcon()101 public ImageResource getIcon() 102 { 103 return new ImageResource2x(res_.iconAccessibility2x()); 104 } 105 106 @Override getName()107 public String getName() 108 { 109 return "Accessibility"; 110 } 111 112 @Override initialize(UserPrefs prefs)113 protected void initialize(UserPrefs prefs) 114 { 115 initialScreenReaderEnabled_ = prefs.enableScreenReader().getValue(); 116 initialShowFocusRectangles_ = prefs.showFocusRectangles().getValue(); 117 chkScreenReaderEnabled_.setValue(initialScreenReaderEnabled_); 118 chkShowFocusRectangles_.setValue(initialShowFocusRectangles_); 119 chkTabMovesFocus_.setValue(prefs.tabKeyMoveFocus().getValue()); 120 populateAnnouncementList(); 121 } 122 123 @Override onApply(UserPrefs prefs)124 public RestartRequirement onApply(UserPrefs prefs) 125 { 126 RestartRequirement restartRequirement = super.onApply(prefs); 127 128 boolean screenReaderEnabledSetting = chkScreenReaderEnabled_.getValue(); 129 if (screenReaderEnabledSetting != initialScreenReaderEnabled_) 130 { 131 initialScreenReaderEnabled_ = screenReaderEnabledSetting; 132 prefs.setScreenReaderEnabled(screenReaderEnabledSetting); 133 restartRequirement.setRestartRequired(); 134 } 135 136 if (chkShowFocusRectangles_.getValue() != initialShowFocusRectangles_) 137 { 138 initialShowFocusRectangles_ = chkShowFocusRectangles_.getValue(); 139 prefs.showFocusRectangles().setGlobalValue(chkShowFocusRectangles_.getValue()); 140 restartRequirement.setRestartRequired(); 141 } 142 143 prefs.tabKeyMoveFocus().setGlobalValue(chkTabMovesFocus_.getValue()); 144 prefs.syncToggleTabKeyMovesFocusState(chkTabMovesFocus_.getValue()); 145 146 if (applyAnnouncementList(prefs)) 147 restartRequirement.setUiReloadRequired(true); 148 149 return restartRequirement; 150 } 151 152 @Override validate()153 public boolean validate() 154 { 155 return (!chkScreenReaderEnabled_.getValue() || 156 (typingStatusDelay_.validate() && maxOutput_.validate())); 157 } 158 populateAnnouncementList()159 private void populateAnnouncementList() 160 { 161 announcements_.clearItems(); 162 for (Map.Entry<String,String> entry : ariaLive_.getAnnouncements().entrySet()) 163 { 164 CheckBox checkBox = new CheckBox(entry.getValue()); 165 checkBox.setFormValue(entry.getKey()); 166 announcements_.addItem(checkBox); 167 168 // The preference tracks disabled announcements, but the UI shows enabled announcements. 169 // Having the UI show disabled announcements is counter-intuitive, but tracking 170 // disabled items in the preferences causes newly added announcements to be enabled 171 // by default. 172 checkBox.setValue(!ariaLive_.isDisabled(entry.getKey())); 173 } 174 } 175 applyAnnouncementList(UserPrefs prefs)176 private boolean applyAnnouncementList(UserPrefs prefs) 177 { 178 boolean origConsoleLog = ariaLive_.isDisabled(AriaLiveService.CONSOLE_LOG); 179 boolean origConsoleCommand = ariaLive_.isDisabled(AriaLiveService.CONSOLE_COMMAND); 180 boolean restartNeeded = false; 181 182 JsArrayString settings = prefs.disabledAriaLiveAnnouncements().getValue(); 183 settings.setLength(0); 184 for (int i = 0; i < announcements_.getItemCount(); i++) 185 { 186 CheckBox chk = announcements_.getItemAtIdx(i); 187 if (!chk.getValue()) // preference tracks disabled, UI tracks enabled 188 settings.push(chk.getFormValue()); 189 190 if (StringUtil.equals(chk.getFormValue(), AriaLiveService.CONSOLE_LOG) && 191 origConsoleLog == chk.getValue()) 192 { 193 restartNeeded = true; 194 } 195 else if (StringUtil.equals(chk.getFormValue(), AriaLiveService.CONSOLE_COMMAND) && 196 origConsoleCommand == chk.getValue()) 197 { 198 restartNeeded = true; 199 } 200 } 201 202 prefs.disabledAriaLiveAnnouncements().setGlobalValue(settings); 203 return restartNeeded; 204 } 205 206 private final CheckBox chkScreenReaderEnabled_; 207 private final CheckBox chkShowFocusRectangles_; 208 private final NumericValueWidget typingStatusDelay_; 209 private final NumericValueWidget maxOutput_; 210 private final CheckBox chkTabMovesFocus_; 211 private final CheckBoxList announcements_; 212 213 // initial values of prefs that can trigger reloads (to avoid unnecessary reloads) 214 private boolean initialScreenReaderEnabled_; 215 private boolean initialShowFocusRectangles_; 216 217 private final PreferencesDialogResources res_; 218 private final AriaLiveService ariaLive_; 219 } 220