// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This dialog walks a user through the flow of setting up Switch
* Access.
*/
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_style.css.js';
import '../controls/settings_slider.js';
import '../os_settings_icons.html.js';
import './switch_access_action_assignment_pane.js';
import {PrefsMixin} from '/shared/settings/prefs/prefs_mixin.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {CrRadioGroupElement} from 'chrome://resources/ash/common/cr_elements/cr_radio_group/cr_radio_group.js';
import {SliderTick} from 'chrome://resources/ash/common/cr_elements/cr_slider/cr_slider.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {castExists} from '../assert_extras.js';
import {Router, routes} from '../router.js';
import {SettingsSwitchAccessActionAssignmentPaneElement} from './switch_access_action_assignment_pane.js';
import {actionToPref, AssignmentContext, AUTO_SCAN_ENABLED_PREF, AUTO_SCAN_KEYBOARD_SPEED_PREF, AUTO_SCAN_SPEED_PREF, AUTO_SCAN_SPEED_RANGE_MS, DEFAULT_AUTO_SCAN_SPEED_MS, SwitchAccessCommand} from './switch_access_constants.js';
import {getTemplate} from './switch_access_setup_guide_dialog.html.js';
import {SwitchAccessSubpageBrowserProxyImpl} from './switch_access_subpage_browser_proxy.js';
/**
* Elements that can be hidden or shown for each setup page.
* The string value should match the element ID in the HTML.
*/
enum SetupElement {
BLUETOOTH_BUTTON = 'bluetooth',
DONE_BUTTON = 'done',
NEXT_BUTTON = 'next',
PREVIOUS_BUTTON = 'previous',
START_OVER_BUTTON = 'startOver',
INTRO_CONTENT = 'intro',
ASSIGN_SWITCH_CONTENT = 'assignSwitch',
AUTO_SCAN_ENABLED_CONTENT = 'autoScanEnabled',
CHOOSE_SWITCH_COUNT_CONTENT = 'chooseSwitchCount',
AUTO_SCAN_SPEED_CONTENT = 'autoScanSpeed',
CLOSING_CONTENT = 'closing',
}
/**
* The IDs of each page in the setup flow.
*/
enum SetupPageId {
INTRO = 0,
ASSIGN_SELECT = 1,
AUTO_SCAN_ENABLED = 2,
CHOOSE_SWITCH_COUNT = 3,
AUTO_SCAN_SPEED = 4,
ASSIGN_NEXT = 5,
ASSIGN_PREVIOUS = 6,
TIC_TAC_TOE = 7,
CLOSING = 8,
}
/**
* Defines what is visible onscreen for a given page of the setup guide.
*/
interface SetupPage {
titleId: string;
visibleElements: SetupElement[];
}
/**
* A dictionary of all of the dialog pages.
*/
const SetupPageList: {[key in SetupPageId]?: SetupPage} = {
[SetupPageId.INTRO]: {
titleId: 'switchAccessSetupIntroTitle',
visibleElements: [
SetupElement.BLUETOOTH_BUTTON,
SetupElement.NEXT_BUTTON,
SetupElement.INTRO_CONTENT,
],
},
[SetupPageId.ASSIGN_SELECT]: {
titleId: 'switchAccessSetupAssignSelectTitle',
visibleElements: [SetupElement.ASSIGN_SWITCH_CONTENT],
},
[SetupPageId.AUTO_SCAN_ENABLED]: {
titleId: 'switchAccessSetupAutoScanEnabledTitle',
visibleElements: [
SetupElement.NEXT_BUTTON,
SetupElement.PREVIOUS_BUTTON,
SetupElement.AUTO_SCAN_ENABLED_CONTENT,
],
},
[SetupPageId.CHOOSE_SWITCH_COUNT]: {
titleId: 'switchAccessSetupChooseSwitchCountTitle',
visibleElements: [
SetupElement.NEXT_BUTTON,
SetupElement.PREVIOUS_BUTTON,
SetupElement.CHOOSE_SWITCH_COUNT_CONTENT,
],
},
[SetupPageId.AUTO_SCAN_SPEED]: {
titleId: 'switchAccessSetupAutoScanSpeedTitle',
visibleElements: [
SetupElement.NEXT_BUTTON,
SetupElement.PREVIOUS_BUTTON,
SetupElement.AUTO_SCAN_SPEED_CONTENT,
],
},
[SetupPageId.ASSIGN_NEXT]: {
titleId: 'switchAccessSetupAssignNextTitle',
visibleElements:
[SetupElement.PREVIOUS_BUTTON, SetupElement.ASSIGN_SWITCH_CONTENT],
},
[SetupPageId.ASSIGN_PREVIOUS]: {
titleId: 'switchAccessSetupAssignPreviousTitle',
visibleElements:
[SetupElement.PREVIOUS_BUTTON, SetupElement.ASSIGN_SWITCH_CONTENT],
},
[SetupPageId.CLOSING]: {
titleId: 'switchAccessSetupClosingTitle',
visibleElements: [
SetupElement.DONE_BUTTON,
SetupElement.START_OVER_BUTTON,
SetupElement.CLOSING_CONTENT,
],
},
};
export interface SettingsSwitchAccessSetupGuideDialogElement {
$: {
chooseSwitchCount: HTMLElement,
closingInstructions: HTMLElement,
titleText: HTMLElement,
switchAccessSetupGuideDialog: CrDialogElement,
switchCountGroup: CrRadioGroupElement,
};
}
const SettingsSwitchAccessSetupGuideDialogElementBase =
PrefsMixin(I18nMixin(PolymerElement));
export class SettingsSwitchAccessSetupGuideDialogElement extends
SettingsSwitchAccessSetupGuideDialogElementBase {
static get is() {
return 'settings-switch-access-setup-guide-dialog';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
autoScanSpeedRangeMs_: {
type: Array,
value: [],
},
currentPageId_: {
type: Number,
value: SetupPageId.INTRO,
},
/**
* A number formatter, to display values with exactly 1 digit after the
* decimal (that has been internationalized properly).
*/
formatter_: {
type: Object,
value() {
// navigator.language actually returns a locale, not just a language.
const locale = window.navigator.language;
const options = {minimumFractionDigits: 1, maximumFractionDigits: 1};
return new Intl.NumberFormat(locale, options);
},
},
maxScanSpeedMs_: {
readOnly: true,
type: Number,
value: AUTO_SCAN_SPEED_RANGE_MS[AUTO_SCAN_SPEED_RANGE_MS.length - 1],
},
maxScanSpeedLabelSec_: {
readOnly: true,
type: String,
},
minScanSpeedMs_: {
readOnly: true,
type: Number,
value: AUTO_SCAN_SPEED_RANGE_MS[0],
},
minScanSpeedLabelSec_: {
readOnly: true,
type: String,
},
switchCount_: {
type: Number,
value: 1,
},
switchToAssign_: {
type: String,
value: null,
},
};
}
static get observers() {
return [
`onSwitchAssignmentMaybeChanged_(
prefs.settings.a11y.switch_access.next.*,
prefs.settings.a11y.switch_access.previous.*,
prefs.settings.a11y.switch_access.select.*)`,
];
}
private autoScanSpeedRangeMs_: SliderTick[];
private currentPageId_: number;
private formatter_: Intl.NumberFormat;
private maxScanSpeedLabelSec_: string;
private maxScanSpeedMs_: number;
private minScanSpeedLabelSec_: string;
private minScanSpeedMs_: number;
private switchCount_: number;
private switchToAssign_: SwitchAccessCommand|null;
constructor() {
super();
this.maxScanSpeedLabelSec_ =
this.scanSpeedStringInSec_(this.maxScanSpeedMs_);
this.minScanSpeedLabelSec_ =
this.scanSpeedStringInSec_(this.minScanSpeedMs_);
this.autoScanSpeedRangeMs_ =
this.ticksWithLabelsInSec_(AUTO_SCAN_SPEED_RANGE_MS);
}
override connectedCallback(): void {
super.connectedCallback();
SwitchAccessSubpageBrowserProxyImpl.getInstance()
.notifySwitchAccessSetupGuideAttached();
}
override ready(): void {
super.ready();
this.addEventListener('exit-pane', this.onSwitchAssignmentMaybeChanged_);
// Reset all switch assignments.
for (const pref of Object.values(actionToPref)) {
chrome.settingsPrivate.setPref(pref, {});
}
// Reset auto-scan.
chrome.settingsPrivate.setPref(
AUTO_SCAN_SPEED_PREF, DEFAULT_AUTO_SCAN_SPEED_MS);
chrome.settingsPrivate.setPref(
AUTO_SCAN_KEYBOARD_SPEED_PREF, DEFAULT_AUTO_SCAN_SPEED_MS);
chrome.settingsPrivate.setPref(AUTO_SCAN_ENABLED_PREF, false);
}
private loadPage_(id: SetupPageId): void {
this.addOrRemoveAssignmentPane_(id);
const newPage = castExists(SetupPageList[id]);
this.$.titleText.textContent = this.i18n(newPage.titleId);
for (const elementId of Object.values(SetupElement)) {
this.shadowRoot!.getElementById(elementId)!.hidden =
!newPage.visibleElements.includes(elementId);
}
this.currentPageId_ = id;
}
/**
* The assignment pane prevents Switch Access from receiving key events when
* it is attached, which disables the user's navigational control. Therefore,
* we add the assignment pane only when it's about to be displayed, and remove
* it as soon as it's complete.
*/
private addOrRemoveAssignmentPane_(id: SetupPageId): void {
let action: SwitchAccessCommand|undefined;
switch (id) {
case SetupPageId.ASSIGN_SELECT:
action = SwitchAccessCommand.SELECT;
break;
case SetupPageId.ASSIGN_NEXT:
action = SwitchAccessCommand.NEXT;
break;
case SetupPageId.ASSIGN_PREVIOUS:
action = SwitchAccessCommand.PREVIOUS;
}
if (action) {
this.initializeAssignmentPane_(action);
} else {
this.removeAssignmentPaneIfPresent_();
}
}
private initializeAssignmentPane_(action: SwitchAccessCommand): void {
this.removeAssignmentPaneIfPresent_();
this.switchToAssign_ = action;
const assignmentPane =
document.createElement('settings-switch-access-action-assignment-pane');
assignmentPane.action = action;
assignmentPane.context = AssignmentContext.SETUP_GUIDE;
this.assignmentContentsElement.appendChild(assignmentPane);
}
private removeAssignmentPaneIfPresent_(): void {
if (this.assignmentContentsElement.firstChild) {
this.assignmentContentsElement.removeChild(
this.assignmentContentsElement.firstChild);
}
this.switchToAssign_ = null;
}
/**
* Determines what page is shown next, from the current page ID and other
* state.
*/
private getNextPageId_(): SetupPageId {
switch (this.currentPageId_) {
case SetupPageId.INTRO:
return SetupPageId.ASSIGN_SELECT;
case SetupPageId.ASSIGN_SELECT:
return SetupPageId.AUTO_SCAN_ENABLED;
case SetupPageId.AUTO_SCAN_ENABLED:
return SetupPageId.CHOOSE_SWITCH_COUNT;
case SetupPageId.CHOOSE_SWITCH_COUNT:
if (this.switchCount_ === 3 || this.switchCount_ === 2) {
return SetupPageId.ASSIGN_NEXT;
} else {
return SetupPageId.AUTO_SCAN_SPEED;
}
case SetupPageId.ASSIGN_NEXT:
if (this.switchCount_ === 3) {
return SetupPageId.ASSIGN_PREVIOUS;
} else {
return SetupPageId.CLOSING;
}
case SetupPageId.ASSIGN_PREVIOUS:
case SetupPageId.AUTO_SCAN_SPEED:
default:
return SetupPageId.CLOSING;
}
}
/**
* Returns what page was shown previously from the current page ID.
*/
private getPreviousPageId_(): SetupPageId {
switch (this.currentPageId_) {
case SetupPageId.CLOSING:
if (this.switchCount_ === 3) {
return SetupPageId.ASSIGN_PREVIOUS;
} else if (this.switchCount_ === 2) {
return SetupPageId.ASSIGN_NEXT;
} else {
return SetupPageId.AUTO_SCAN_SPEED;
}
case SetupPageId.ASSIGN_PREVIOUS:
return SetupPageId.ASSIGN_NEXT;
case SetupPageId.ASSIGN_NEXT:
case SetupPageId.AUTO_SCAN_SPEED:
return SetupPageId.CHOOSE_SWITCH_COUNT;
case SetupPageId.CHOOSE_SWITCH_COUNT:
return SetupPageId.AUTO_SCAN_ENABLED;
case SetupPageId.AUTO_SCAN_ENABLED:
return SetupPageId.ASSIGN_SELECT;
case SetupPageId.ASSIGN_SELECT:
default:
return SetupPageId.INTRO;
}
}
private onExitClick_(): void {
this.$.switchAccessSetupGuideDialog.close();
}
private onStartOverClick_(): void {
this.loadPage_(SetupPageId.INTRO);
}
private onNextClick_(): void {
this.loadPage_(this.getNextPageId_());
// Enable auto-scan when we reach that page of the setup guide.
if (this.currentPageId_ === SetupPageId.AUTO_SCAN_ENABLED) {
chrome.settingsPrivate.setPref(AUTO_SCAN_ENABLED_PREF, true);
}
// Disable auto-scan once the user has selected two or more switches.
if (this.currentPageId_ === SetupPageId.ASSIGN_NEXT) {
chrome.settingsPrivate.setPref(AUTO_SCAN_ENABLED_PREF, false);
}
if (this.currentPageId_ === SetupPageId.CLOSING) {
if (this.switchCount_ >= 2) {
this.$.closingInstructions.textContent =
this.i18n('switchAccessSetupClosingManualScanInstructions');
}
}
}
private onPreviousClick_(): void {
// Disable auto-scan when the user reverses to before it was enabled.
if (this.currentPageId_ === SetupPageId.AUTO_SCAN_ENABLED) {
chrome.settingsPrivate.setPref(AUTO_SCAN_ENABLED_PREF, false);
}
this.loadPage_(this.getPreviousPageId_());
}
private onBluetoothClick_(): void {
Router.getInstance().navigateTo(routes.BLUETOOTH_DEVICES);
this.$.switchAccessSetupGuideDialog.close();
}
private onAutoScanSpeedFaster_(): void {
const pref = this.getPref<number>(AUTO_SCAN_SPEED_PREF);
const currentValue = pref.value;
// Find the first element in the array that is equal to, or smaller than,
// the current value. Since AUTO_SCAN_SPEED_RANGE_MS is sorted largest to
// smallest, this gives us a guarantee that we are within one step (100ms)
// of the value at the provided index.
const index =
AUTO_SCAN_SPEED_RANGE_MS.findIndex(elem => elem <= currentValue);
if (index === -1 || index === AUTO_SCAN_SPEED_RANGE_MS.length - 1) {
return;
}
chrome.settingsPrivate.setPref(
AUTO_SCAN_SPEED_PREF, AUTO_SCAN_SPEED_RANGE_MS[index + 1]);
}
private onAutoScanSpeedSlower_(): void {
const pref = this.getPref<number>(AUTO_SCAN_SPEED_PREF);
const currentValue = pref.value;
// Find the first element in the array that is equal to, or smaller than,
// the current value. Since AUTO_SCAN_SPEED_RANGE_MS is sorted largest to
// smallest, this gives us a guarantee that we are within one step (100ms)
// of the value at the provided index.
const index =
AUTO_SCAN_SPEED_RANGE_MS.findIndex(elem => elem <= currentValue);
if (index <= 0) {
return;
}
chrome.settingsPrivate.setPref(
AUTO_SCAN_SPEED_PREF, AUTO_SCAN_SPEED_RANGE_MS[index - 1]);
}
private onSwitchCountChanged_(): void {
const selected = this.$.switchCountGroup.selected;
if (selected === 'one-switch') {
this.switchCount_ = 1;
} else if (selected === 'two-switches') {
this.switchCount_ = 2;
} else if (selected === 'three-switches') {
this.switchCount_ = 3;
}
}
private onSwitchAssignmentMaybeChanged_(): void {
if (!this.assignmentContentsElement ||
!this.assignmentContentsElement.firstChild) {
return;
}
const currentAction = (this.assignmentContentsElement.firstChild as
SettingsSwitchAccessActionAssignmentPaneElement)
.action;
const pref =
this.getPref<Record<string, string[]>>(actionToPref[currentAction]);
const hasSwitchAssigned = Object.keys(pref.value).length > 0;
if (hasSwitchAssigned) {
this.onNextClick_();
} else {
this.initializeAssignmentPane_(currentAction);
}
}
private scanSpeedStringInSec_(scanSpeedValueMs: number): string {
const scanSpeedValueSec = scanSpeedValueMs / 1000;
return this.i18n(
'durationInSeconds', this.formatter_.format(scanSpeedValueSec));
}
private ticksWithLabelsInSec_(ticksInMs: number[]): SliderTick[] {
// Dividing by 1000 to convert milliseconds to seconds for the label.
return ticksInMs.map(
x => ({label: `${this.scanSpeedStringInSec_(x)}`, value: x}));
}
private get assignmentContentsElement(): HTMLElement {
return castExists(
this.shadowRoot!.getElementById(SetupElement.ASSIGN_SWITCH_CONTENT)!
.querySelector('.sa-setup-contents'));
}
private getAssignSwitchIllo_(): string {
switch (this.switchToAssign_) {
case SwitchAccessCommand.SELECT:
return 'os-settings-illo:switch-access-setup-guide-assign-select';
case SwitchAccessCommand.NEXT:
return 'os-settings-illo:switch-access-setup-guide-assign-next';
case SwitchAccessCommand.PREVIOUS:
return 'os-settings-illo:switch-access-setup-guide-assign-previous';
default:
return '';
}
}
private getSwitchCountIllo_(): string {
switch (this.switchCount_) {
case 1:
return 'os-settings-illo:switch-access-setup-guide-choose-1-switch';
case 2:
return 'os-settings-illo:switch-access-setup-guide-choose-2-switches';
case 3:
return 'os-settings-illo:switch-access-setup-guide-choose-3-switches';
default:
return '';
}
}
}
declare global {
interface HTMLElementTagNameMap {
'settings-switch-access-setup-guide-dialog':
SettingsSwitchAccessSetupGuideDialogElement;
}
}
customElements.define(
SettingsSwitchAccessSetupGuideDialogElement.is,
SettingsSwitchAccessSetupGuideDialogElement);