blob: 23ec8afc2721372009b87ab45a0815941974a7bf [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview ChromeVox options page.
*/
import {constants} from '../../common/constants.js';
import {AbstractTts} from '../common/abstract_tts.js';
import {BackgroundBridge} from '../common/background_bridge.js';
import {BrailleTable} from '../common/braille/braille_table.js';
import {ExtensionBridge} from '../common/extension_bridge.js';
import {Msgs} from '../common/msgs.js';
import {PanelCommand, PanelCommandType} from '../common/panel_command.js';
import {BluetoothBrailleDisplayUI} from './bluetooth_braille_display_ui.js';
/** @const {string} */
const GOOGLE_TTS_EXTENSION_ID = 'gjjabgpgjpampikjhjpfhneeoapjbjaf';
/** @const {string} */
const ESPEAK_TTS_EXTENSION_ID = 'dakbfdmgjiabojdgbiljlhgjbokobjpg';
/**
* Class to manage the options page.
*/
export class OptionsPage {
/**
* Initialize the options page by setting the current value of all prefs, and
* adding event listeners.
* @this {OptionsPage}
*/
static async init() {
OptionsPage.populateVoicesSelect();
BrailleTable.getAll(function(tables) {
/** @type {!Array<BrailleTable.Table>} */
OptionsPage.brailleTables = tables;
OptionsPage.populateBrailleTablesSelect();
});
chrome.storage.local.get({'brailleWordWrap': true}, function(items) {
$('brailleWordWrap').checked = items.brailleWordWrap;
});
chrome.storage.local.get({'virtualBrailleRows': 1}, function(items) {
$('virtual_braille_display_rows_input').value =
items['virtualBrailleRows'];
});
chrome.storage.local.get({'virtualBrailleColumns': 40}, function(items) {
$('virtual_braille_display_columns_input').value =
items['virtualBrailleColumns'];
});
const changeToInterleave =
Msgs.getMsg('options_change_current_display_style_interleave');
const changeToSideBySide =
Msgs.getMsg('options_change_current_display_style_side_by_side');
const currentlyDisplayingInterleave =
Msgs.getMsg('options_current_display_style_interleave');
const currentlyDisplayingSideBySide =
Msgs.getMsg('options_current_display_style_side_by_side');
$('changeDisplayStyle').textContent =
localStorage['brailleSideBySide'] === 'true' ? changeToInterleave :
changeToSideBySide;
$('currentDisplayStyle').textContent =
localStorage['brailleSideBySide'] === 'true' ?
currentlyDisplayingSideBySide :
currentlyDisplayingInterleave;
const showEventStreamFilters =
Msgs.getMsg('options_show_event_stream_filters');
const hideEventStreamFilters =
Msgs.getMsg('options_hide_event_stream_filters');
$('toggleEventStreamFilters').textContent = showEventStreamFilters;
OptionsPage.disableEventStreamFilterCheckBoxes(
localStorage['enableEventStreamLogging'] === 'false');
if (localStorage['audioStrategy']) {
for (let i = 0, opt; opt = $('audioStrategy').options[i]; i++) {
if (opt.id === localStorage['audioStrategy']) {
opt.setAttribute('selected', '');
}
}
}
if (localStorage['capitalStrategy']) {
for (let i = 0, opt; opt = $('capitalStrategy').options[i]; ++i) {
if (opt.id === localStorage['capitalStrategy']) {
opt.setAttribute('selected', '');
}
}
}
if (localStorage['numberReadingStyle']) {
for (let i = 0, opt; opt = $('numberReadingStyle').options[i]; ++i) {
if (opt.id === localStorage['numberReadingStyle']) {
opt.setAttribute('selected', '');
}
}
}
if (localStorage[AbstractTts.PUNCTUATION_ECHO]) {
const currentPunctuationEcho =
AbstractTts
.PUNCTUATION_ECHOES[localStorage[AbstractTts.PUNCTUATION_ECHO]];
for (let i = 0, opt; opt = $('punctuationEcho').options[i]; ++i) {
if (opt.id === currentPunctuationEcho.name) {
opt.setAttribute('selected', '');
}
}
}
$('toggleEventStreamFilters').addEventListener('click', function(evt) {
if ($('eventStreamFilters').hidden) {
$('eventStreamFilters').hidden = false;
$('toggleEventStreamFilters').textContent = hideEventStreamFilters;
} else {
$('eventStreamFilters').hidden = true;
$('toggleEventStreamFilters').textContent = showEventStreamFilters;
}
});
$('openTtsSettings').addEventListener('click', evt => {
chrome.accessibilityPrivate.openSettingsSubpage(
'manageAccessibility/tts');
});
$('enableAllEventStreamFilters').addEventListener('click', () => {
OptionsPage.setAllEventStreamLoggingFilters(true);
});
$('disableAllEventStreamFilters').addEventListener('click', () => {
OptionsPage.setAllEventStreamLoggingFilters(false);
});
$('chromeVoxDeveloperOptions').addEventListener('expanded-changed', () => {
const hidden = !$('chromeVoxDeveloperOptions')['expanded'];
$('developerSpeechLogging').hidden = hidden;
$('developerEarconLogging').hidden = hidden;
$('developerBrailleLogging').hidden = hidden;
$('developerEventStream').hidden = hidden;
$('showDeveloperLog').hidden = hidden;
});
$('openDeveloperLog').addEventListener('click', function(evt) {
const logPage = {url: 'chromevox/log_page/log.html'};
chrome.tabs.create(logPage);
});
Msgs.addTranslatedMessagesToDom(document);
OptionsPage.hidePlatformSpecifics();
await OptionsPage.update();
document.addEventListener('change', OptionsPage.eventListener, false);
document.addEventListener('click', OptionsPage.eventListener, false);
document.addEventListener('keydown', OptionsPage.eventListener, false);
window.addEventListener('storage', event => {
if (event.key === 'speakTextUnderMouse') {
chrome.accessibilityPrivate.enableMouseEvents(
event.newValue === String(true));
}
});
ExtensionBridge.addMessageListener(function(message) {
if (message['prefs']) {
OptionsPage.update();
}
});
const clearVirtualDisplay = function() {
const groups = [];
const sizeOfDisplay =
parseInt($('virtual_braille_display_rows_input').innerHTML, 10) *
parseInt($('virtual_braille_display_columns_input').innerHTML, 10);
for (let i = 0; i < sizeOfDisplay; i++) {
groups.push(['X', 'X']);
}
(new PanelCommand(PanelCommandType.UPDATE_BRAILLE, {groups})).send();
};
$('changeDisplayStyle').addEventListener('click', function(evt) {
const sideBySide = localStorage['brailleSideBySide'] !== 'true';
localStorage['brailleSideBySide'] = sideBySide;
$('changeDisplayStyle').textContent =
sideBySide ? changeToInterleave : changeToSideBySide;
$('currentDisplayStyle').textContent = sideBySide ?
currentlyDisplayingSideBySide :
currentlyDisplayingInterleave;
clearVirtualDisplay();
}, true);
handleNumericalInputPref(
'virtual_braille_display_rows_input', 'virtualBrailleRows');
handleNumericalInputPref(
'virtual_braille_display_columns_input', 'virtualBrailleColumns');
/** @type {!BluetoothBrailleDisplayUI} */
OptionsPage.bluetoothBrailleDisplayUI = new BluetoothBrailleDisplayUI();
const bluetoothBraille = $('bluetoothBraille');
if (bluetoothBraille) {
OptionsPage.bluetoothBrailleDisplayUI.attach(bluetoothBraille);
}
$('usePitchChanges').addEventListener('click', evt => {
// The capitalStrategy pref depends on the value of usePitchChanges.
// When usePitchChanges is toggled, we should update the preference value
// and options for capitalStrategy.
const checked = evt.target.checked;
if (!checked) {
$('announceCapitals').selected = true;
$('increasePitch').selected = false;
$('increasePitch').disabled = true;
localStorage['capitalStrategyBackup'] = localStorage['capitalStrategy'];
BackgroundBridge.ChromeVoxPrefs.setPref(
'capitalStrategy', 'announceCapitals');
} else {
$('increasePitch').disabled = false;
const capitalStrategyBackup = localStorage['capitalStrategyBackup'];
if (capitalStrategyBackup) {
// Restore original capitalStrategy setting.
$('announceCapitals').selected =
(capitalStrategyBackup === 'announceCapitals');
$('increasePitch').selected =
(capitalStrategyBackup === 'increasePitch');
BackgroundBridge.ChromeVoxPrefs.setPref(
'capitalStrategy', capitalStrategyBackup);
}
}
});
}
/**
* Update the value of controls to match the current preferences.
* This happens if the user presses a key in a tab that changes a
* pref.
*/
static async update() {
const prefs = await BackgroundBridge.ChromeVoxPrefs.getPrefs();
for (const key in prefs) {
// TODO(rshearer): 'active' is a pref, but there's no place in the
// options page to specify whether you want ChromeVox active.
const elements = document.querySelectorAll('*[name="' + key + '"]');
for (let i = 0; i < elements.length; i++) {
OptionsPage.setValue(elements[i], prefs[key]);
}
}
}
/**
* Populates the voices select with options.
*/
static async populateVoicesSelect() {
const select = $('voices');
async function setVoiceList() {
const selectedVoice =
await BackgroundBridge.ChromeVoxBackground.getCurrentVoice();
const addVoiceOption = (visibleVoiceName, voiceName) => {
const option = document.createElement('option');
option.voiceName = voiceName;
option.innerText = visibleVoiceName;
if (selectedVoice === voiceName) {
option.setAttribute('selected', '');
}
select.add(option);
};
chrome.tts.getVoices(function(voices) {
select.innerHTML = '';
// TODO(plundblad): voiceName can actually be omitted in the TTS engine.
// We should generate a name in that case.
voices.forEach(function(voice) {
voice.voiceName = voice.voiceName || '';
});
voices.sort(function(a, b) {
// Prefer Google tts voices over all others.
if (a.extensionId === GOOGLE_TTS_EXTENSION_ID &&
b.extensionId !== GOOGLE_TTS_EXTENSION_ID) {
return -1;
}
// Next, prefer Espeak tts voices.
if (a.extensionId === ESPEAK_TTS_EXTENSION_ID &&
b.extensionId !== ESPEAK_TTS_EXTENSION_ID) {
return -1;
}
// Finally, prefer local over remote voices.
if (!a['remote'] && b['remote']) {
return -1;
}
return 0;
});
addVoiceOption(Msgs.getMsg('system_voice'), constants.SYSTEM_VOICE);
voices.forEach(voice => {
addVoiceOption(voice.voiceName, voice.voiceName);
});
});
}
window.speechSynthesis.onvoiceschanged = setVoiceList;
await setVoiceList();
select.addEventListener('change', function(evt) {
const selIndex = select.selectedIndex;
const sel = select.options[selIndex];
chrome.storage.local.set({voiceName: sel.voiceName});
}, true);
}
/**
* Populates the braille select control.
*/
static populateBrailleTablesSelect() {
const tables = OptionsPage.brailleTables;
const populateSelect = function(node, dots) {
const activeTable = localStorage[node.id] || localStorage['brailleTable'];
// Gather the display names and sort them according to locale.
const items = [];
for (let i = 0, table; table = tables[i]; i++) {
if (table.dots !== dots) {
continue;
}
const displayName = BrailleTable.getDisplayName(table);
// Ignore tables that don't have a display name.
if (displayName) {
items.push({id: table.id, name: displayName});
}
}
items.sort(function(a, b) {
return a.id.localeCompare(b.id);
});
for (let i = 0, item; item = items[i]; ++i) {
const elem = document.createElement('option');
elem.id = item.id;
elem.textContent = item.name;
if (item.id === activeTable) {
elem.setAttribute('selected', '');
}
node.appendChild(elem);
}
};
const select6 = $('brailleTable6');
const select8 = $('brailleTable8');
populateSelect(select6, '6');
populateSelect(select8, '8');
const handleBrailleSelect = function(node) {
return function(evt) {
const selIndex = node.selectedIndex;
const sel = node.options[selIndex];
localStorage['brailleTable'] = sel.id;
localStorage[node.id] = sel.id;
BackgroundBridge.BrailleBackground.refreshBrailleTable(
localStorage['brailleTable']);
};
};
select6.addEventListener('change', handleBrailleSelect(select6), true);
select8.addEventListener('change', handleBrailleSelect(select8), true);
const tableTypeButton = $('brailleTableType');
const updateTableType = function(setFocus) {
const currentTableType =
localStorage['brailleTableType'] || 'brailleTable6';
if (currentTableType === 'brailleTable6') {
select6.parentElement.style.display = 'block';
select8.parentElement.style.display = 'none';
if (setFocus) {
select6.focus();
}
localStorage['brailleTable'] = localStorage['brailleTable6'];
localStorage['brailleTableType'] = 'brailleTable6';
tableTypeButton.textContent =
Msgs.getMsg('options_braille_table_type_6');
} else {
select6.parentElement.style.display = 'none';
select8.parentElement.style.display = 'block';
if (setFocus) {
select8.focus();
}
localStorage['brailleTable'] = localStorage['brailleTable8'];
localStorage['brailleTableType'] = 'brailleTable8';
tableTypeButton.textContent =
Msgs.getMsg('options_braille_table_type_8');
}
BackgroundBridge.BrailleBackground.refreshBrailleTable(
localStorage['brailleTable']);
};
updateTableType(false);
tableTypeButton.addEventListener('click', function(evt) {
const oldTableType = localStorage['brailleTableType'];
localStorage['brailleTableType'] =
oldTableType === 'brailleTable6' ? 'brailleTable8' : 'brailleTable6';
updateTableType(true);
}, true);
}
/**
* Set the html element for a preference to match the given value.
* @param {Element} element The HTML control.
* @param {string} value The new value.
*/
static setValue(element, value) {
if (element.tagName === 'INPUT' && element.type === 'checkbox') {
element.checked = (value === 'true');
} else if (element.tagName === 'INPUT' && element.type === 'radio') {
element.checked = (String(element.value) === value);
} else {
element.value = value;
}
}
/**
* Disable event stream logging filter check boxes.
* Check boxes should be disabled when event stream logging is disabled.
* @param {boolean} disable
*/
static disableEventStreamFilterCheckBoxes(disable) {
const filters = document.querySelectorAll('.option-eventstream > input');
for (let i = 0; i < filters.length; i++) {
filters[i].disabled = disable;
}
}
/**
* Set all event stream logging filter to on or off.
* @param {boolean} enabled
*/
static setAllEventStreamLoggingFilters(enabled) {
for (const checkbox of document.querySelectorAll(
'.option-eventstream > input')) {
if (checkbox.checked !== enabled) {
OptionsPage.setEventStreamFilter(checkbox.name, enabled);
}
}
}
/**
* Set the specified event logging filter to on or off.
* @param {string} name
* @param {boolean} enabled
*/
static setEventStreamFilter(name, enabled) {
BackgroundBridge.ChromeVoxPrefs.setPref(name, enabled);
// TODO(accessibility): the below cast needs to be validated.
BackgroundBridge.EventStreamLogger.notifyEventStreamFilterChanged(
/** @type {chrome.automation.EventType} */ (name), enabled);
}
/**
* Event listener, called when an event occurs in the page that might
* affect one of the preference controls.
* @param {Event} event The event.
* @return {boolean} True if the default action should occur.
*/
static eventListener(event) {
setTimeout(function() {
const target = event.target;
if (target.id === 'brailleWordWrap') {
chrome.storage.local.set({brailleWordWrap: target.checked});
} else if (target.className.indexOf('logging') !== -1) {
BackgroundBridge.ChromeVoxPrefs.setLoggingPrefs(
target.name, target.checked);
if (target.name === 'enableEventStreamLogging') {
OptionsPage.disableEventStreamFilterCheckBoxes(!target.checked);
}
} else if (target.className.indexOf('eventstream') !== -1) {
OptionsPage.setEventStreamFilter(target.name, target.checked);
} else if (target.id === 'punctuationEcho') {
const selectedPunctuationEcho = target.options[target.selectedIndex].id;
const punctuationEcho = AbstractTts.PUNCTUATION_ECHOES.findIndex(
echo => echo.name === selectedPunctuationEcho);
BackgroundBridge.ChromeVoxState.updatePunctuationEcho(punctuationEcho);
} else if (target.classList.contains('pref')) {
if (target.tagName === 'INPUT' && target.type === 'checkbox') {
BackgroundBridge.ChromeVoxPrefs.setPref(target.name, target.checked);
} else if (target.tagName === 'INPUT' && target.type === 'radio') {
const key = target.name;
const elements = document.querySelectorAll('*[name="' + key + '"]');
for (let i = 0; i < elements.length; i++) {
if (elements[i].checked) {
BackgroundBridge.ChromeVoxPrefs.setPref(
target.name, elements[i].value);
}
}
} else if (target.tagName === 'SELECT') {
const selIndex = target.selectedIndex;
const sel = target.options[selIndex];
const value = sel ? sel.id : 'audioNormal';
BackgroundBridge.ChromeVoxPrefs.setPref(target.id, value);
}
}
}, 0);
return true;
}
/**
* Hides all elements not matching the current platform.
*/
static hidePlatformSpecifics() {}
}
/**
* Adds event listeners to input boxes to update local storage values and
* make sure that the input is a positive nonempty number between 1 and 99.
* @param {string} id Id of the input box.
* @param {string} pref Preference key in localStorage to access and modify.
*/
const handleNumericalInputPref = function(id, pref) {
$(id).addEventListener('input', function(evt) {
if ($(id).value === '') {
return;
} else if (
parseInt($(id).value, 10) < 1 || parseInt($(id).value, 10) > 99) {
chrome.storage.local.get(pref, function(items) {
$(id).value = items[pref];
});
} else {
const items = {};
items[pref] = $(id).value;
chrome.storage.local.set(items);
}
}, true);
$(id).addEventListener('focusout', function(evt) {
if ($(id).value === '') {
chrome.storage.local.get(pref, function(items) {
$(id).value = items[pref];
});
}
}, true);
};
document.addEventListener('DOMContentLoaded', async function() {
await OptionsPage.init();
}, false);
window.addEventListener('beforeunload', function(e) {
OptionsPage.bluetoothBrailleDisplayUI.detach();
});
/**
* Shortcut for document.getElementById.
* @param {string} id of the element.
* @return {Element} with the id.
*/
function $(id) {
return document.getElementById(id);
}