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