blob: 461c0435642305091300003fd6998314d8587018 [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.
*
*/
goog.provide('cvox.OptionsPage');
goog.require('cvox.BrailleBackground');
goog.require('cvox.BrailleTable');
goog.require('cvox.ChromeEarcons');
goog.require('cvox.ChromeHost');
goog.require('cvox.ChromeTts');
goog.require('cvox.ChromeVox');
goog.require('cvox.ChromeVoxPrefs');
goog.require('cvox.CommandStore');
goog.require('cvox.ExtensionBridge');
goog.require('cvox.HostFactory');
goog.require('cvox.KeyMap');
goog.require('cvox.KeySequence');
goog.require('cvox.Msgs');
goog.require('cvox.PlatformFilter');
goog.require('cvox.PlatformUtil');
/**
* This object is exported by the main background page.
*/
window.braille;
/**
* Class to manage the options page.
* @constructor
*/
cvox.OptionsPage = function() {
};
/**
* The ChromeVoxPrefs object.
* @type {cvox.ChromeVoxPrefs}
*/
cvox.OptionsPage.prefs;
/**
* A mapping from keycodes to their human readable text equivalents.
* This is initialized in cvox.OptionsPage.init for internationalization.
* @type {Object.<string, string>}
*/
cvox.OptionsPage.KEYCODE_TO_TEXT = {
};
/**
* A mapping from human readable text to keycode values.
* This is initialized in cvox.OptionsPage.init for internationalization.
* @type {Object.<string, string>}
*/
cvox.OptionsPage.TEXT_TO_KEYCODE = {
};
/**
* Initialize the options page by setting the current value of all prefs,
* building the key bindings table, and adding event listeners.
* @suppress {missingProperties} Property prefs never defined on Window
*/
cvox.OptionsPage.init = function() {
cvox.ChromeVox.msgs = new cvox.Msgs();
cvox.OptionsPage.prefs = chrome.extension.getBackgroundPage().prefs;
cvox.OptionsPage.populateKeyMapSelect();
cvox.OptionsPage.addKeys();
cvox.OptionsPage.populateVoicesSelect();
cvox.BrailleTable.getAll(function(tables) {
/** @type {!Array.<cvox.BrailleTable.Table>} */
cvox.OptionsPage.brailleTables = tables;
cvox.OptionsPage.populateBrailleTablesSelect();
});
cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document);
cvox.OptionsPage.hidePlatformSpecifics();
cvox.OptionsPage.update();
document.addEventListener('change', cvox.OptionsPage.eventListener, false);
document.addEventListener('click', cvox.OptionsPage.eventListener, false);
document.addEventListener('keydown', cvox.OptionsPage.eventListener, false);
cvox.ExtensionBridge.addMessageListener(function(message) {
if (message['keyBindings'] || message['prefs']) {
cvox.OptionsPage.update();
}
});
$('selectKeys').addEventListener(
'click', cvox.OptionsPage.reset, false);
if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) {
$('version').textContent =
chrome.app.getDetails().version;
}
};
/**
* 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.
*/
cvox.OptionsPage.update = function() {
var prefs = cvox.OptionsPage.prefs.getPrefs();
for (var 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.
var elements = document.querySelectorAll('*[name="' + key + '"]');
for (var i = 0; i < elements.length; i++) {
cvox.OptionsPage.setValue(elements[i], prefs[key]);
}
}
};
/**
* Populate the keymap select element with stored keymaps
*/
cvox.OptionsPage.populateKeyMapSelect = function() {
var select = $('cvox_keymaps');
for (var id in cvox.KeyMap.AVAILABLE_MAP_INFO) {
var info = cvox.KeyMap.AVAILABLE_MAP_INFO[id];
var option = document.createElement('option');
option.id = id;
option.className = 'i18n';
option.setAttribute('msgid', id);
if (cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id) {
option.setAttribute('selected', '');
}
select.appendChild(option);
}
select.addEventListener('change', cvox.OptionsPage.reset, true);
};
/**
* Add the input elements for the key bindings to the container element
* in the page. They're sorted in order of description.
*/
cvox.OptionsPage.addKeys = function() {
var container = $('keysContainer');
var keyMap = cvox.OptionsPage.prefs.getKeyMap();
cvox.OptionsPage.prevTime = new Date().getTime();
cvox.OptionsPage.keyCount = 0;
container.addEventListener('keypress', goog.bind(function(evt) {
if (evt.target.id == 'cvoxKey') {
return;
}
this.keyCount++;
var currentTime = new Date().getTime();
if (currentTime - this.prevTime > 1000 || this.keyCount > 2) {
if (document.activeElement.id == 'toggleKeyPrefix') {
this.keySequence = new cvox.KeySequence(evt, false);
this.keySequence.keys['ctrlKey'][0] = true;
} else {
this.keySequence = new cvox.KeySequence(evt, true);
}
this.keyCount = 1;
} else {
this.keySequence.addKeyEvent(evt);
}
var keySeqStr = cvox.KeyUtil.keySequenceToString(this.keySequence, true);
var announce = keySeqStr.replace(/\+/g,
' ' + cvox.ChromeVox.msgs.getMsg('then') + ' ');
announce = announce.replace(/>/g,
' ' + cvox.ChromeVox.msgs.getMsg('followed_by') + ' ');
announce = announce.replace('Cvox',
' ' + cvox.ChromeVox.msgs.getMsg('modifier_key') + ' ');
// TODO(dtseng): Only basic conflict detection; it does not speak the
// conflicting command. Nor does it detect prefix conflicts like Cvox+L vs
// Cvox+L>L.
if (cvox.OptionsPage.prefs.setKey(document.activeElement.id,
this.keySequence)) {
document.activeElement.value = keySeqStr;
} else {
announce = cvox.ChromeVox.msgs.getMsg('key_conflict', [announce]);
}
cvox.OptionsPage.speak(announce, cvox.QueueMode.QUEUE);
this.prevTime = currentTime;
evt.preventDefault();
evt.stopPropagation();
}, cvox.OptionsPage), true);
var categories = cvox.CommandStore.categories();
for (var i = 0; i < categories.length; i++) {
// Braille bindings can't be customized, so don't include them.
if (categories[i] == 'braille') {
continue;
}
var headerElement = document.createElement('h3');
headerElement.className = 'i18n';
headerElement.setAttribute('msgid', categories[i]);
headerElement.id = categories[i];
container.appendChild(headerElement);
var commands = cvox.CommandStore.commandsForCategory(categories[i]);
for (var j = 0; j < commands.length; j++) {
var command = commands[j];
// TODO: Someday we may want to have more than one key
// mapped to a command, so we'll need to figure out how to display
// that. For now, just take the first key.
var keySeqObj = keyMap.keyForCommand(command)[0];
// Explicitly skip toggleChromeVox in ChromeOS.
if (command == 'toggleChromeVox' &&
cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.CHROMEOS)) {
continue;
}
var inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.className = 'key active-key';
inputElement.id = command;
var displayedCombo;
if (keySeqObj != null) {
displayedCombo = cvox.KeyUtil.keySequenceToString(keySeqObj, true);
} else {
displayedCombo = '';
}
inputElement.value = displayedCombo;
// Don't allow the user to change the sticky mode or stop speaking key.
if (command == 'toggleStickyMode' || command == 'stopSpeech') {
inputElement.disabled = true;
}
var message = cvox.CommandStore.messageForCommand(command);
if (!message) {
// TODO(dtseng): missing message id's.
message = command;
}
var labelElement = document.createElement('label');
labelElement.className = 'i18n';
labelElement.setAttribute('msgid', message);
labelElement.setAttribute('for', inputElement.id);
var divElement = document.createElement('div');
divElement.className = 'key-container';
container.appendChild(divElement);
divElement.appendChild(inputElement);
divElement.appendChild(labelElement);
}
var brElement = document.createElement('br');
container.appendChild(brElement);
}
if ($('cvoxKey') == null) {
// Add the cvox key field
var inputElement = document.createElement('input');
inputElement.type = 'text';
inputElement.className = 'key';
inputElement.id = 'cvoxKey';
var labelElement = document.createElement('label');
labelElement.className = 'i18n';
labelElement.setAttribute('msgid', 'options_cvox_modifier_key');
labelElement.setAttribute('for', 'cvoxKey');
var modifierSectionSibling =
$('modifier_keys').nextSibling;
var modifierSectionParent = modifierSectionSibling.parentNode;
modifierSectionParent.insertBefore(labelElement, modifierSectionSibling);
modifierSectionParent.insertBefore(inputElement, labelElement);
var cvoxKey = $('cvoxKey');
cvoxKey.value = localStorage['cvoxKey'];
cvoxKey.addEventListener('keydown', function(evt) {
if (!this.modifierSeq_) {
this.modifierCount_ = 0;
this.modifierSeq_ = new cvox.KeySequence(evt, false);
} else {
this.modifierSeq_.addKeyEvent(evt);
}
// Never allow non-modified keys.
if (!this.modifierSeq_.isAnyModifierActive()) {
// Indicate error and instructions excluding tab.
if (evt.keyCode != 9) {
cvox.OptionsPage.speak(
cvox.ChromeVox.msgs.getMsg('modifier_entry_error'),
cvox.QueueMode.FLUSH, {});
}
this.modifierSeq_ = null;
} else {
this.modifierCount_++;
}
// Don't trap tab or shift.
if (!evt.shiftKey && evt.keyCode != 9) {
evt.preventDefault();
evt.stopPropagation();
}
}, true);
cvoxKey.addEventListener('keyup', function(evt) {
if (this.modifierSeq_) {
this.modifierCount_--;
if (this.modifierCount_ == 0) {
var modifierStr =
cvox.KeyUtil.keySequenceToString(this.modifierSeq_, true, true);
evt.target.value = modifierStr;
cvox.OptionsPage.speak(
cvox.ChromeVox.msgs.getMsg('modifier_entry_set', [modifierStr]),
cvox.QueueMode.QUEUE);
localStorage['cvoxKey'] = modifierStr;
this.modifierSeq_ = null;
}
evt.preventDefault();
evt.stopPropagation();
}
}, true);
}
};
/**
* Populates the voices select with options.
*/
cvox.OptionsPage.populateVoicesSelect = function() {
var select = $('voices');
function setVoiceList() {
select.innerHTML = '';
chrome.tts.getVoices(function(voices) {
voices.forEach(function(voice) {
var option = document.createElement('option');
option.voiceName = voice.voiceName || '';
option.innerText = option.voiceName;
chrome.storage.local.get('voiceName', function(items) {
if (items.voiceName == voice.voiceName) {
option.setAttribute('selected', '');
}
});
select.add(option);
});
});
}
window.speechSynthesis.onvoiceschanged = setVoiceList.bind(this);
setVoiceList();
select.addEventListener('change', function(evt) {
var selIndex = select.selectedIndex;
var sel = select.options[selIndex];
chrome.storage.local.set({voiceName: sel.voiceName});
}, true);
};
/**
* Populates the braille select control.
* @this {cvox.OptionsPage}
*/
cvox.OptionsPage.populateBrailleTablesSelect = function() {
if (!cvox.ChromeVox.isChromeOS) {
return;
}
var tables = cvox.OptionsPage.brailleTables;
var populateSelect = function(node, dots) {
var activeTable = localStorage[node.id] || localStorage['brailleTable'];
// Gather the display names and sort them according to locale.
var items = [];
for (var i = 0, table; table = tables[i]; i++) {
if (table.dots !== dots) {
continue;
}
items.push({id: table.id,
name: cvox.BrailleTable.getDisplayName(table)});
}
items.sort(function(a, b) { return a.name.localeCompare(b.name);});
for (var i = 0, item; item = items[i]; ++i) {
var elem = document.createElement('option');
elem.id = item.id;
elem.textContent = item.name;
if (item.id == activeTable) {
elem.setAttribute('selected', '');
}
node.appendChild(elem);
}
};
var select6 = $('brailleTable6');
var select8 = $('brailleTable8');
populateSelect(select6, '6');
populateSelect(select8, '8');
var handleBrailleSelect = function(node) {
return function(evt) {
var selIndex = node.selectedIndex;
var sel = node.options[selIndex];
localStorage['brailleTable'] = sel.id;
localStorage[node.id] = sel.id;
/** @type {cvox.BrailleBackground} */
var braille = chrome.extension.getBackgroundPage().braille;
braille.refreshTranslator();
};
};
select6.addEventListener('change', handleBrailleSelect(select6), true);
select8.addEventListener('change', handleBrailleSelect(select8), true);
var tableTypeButton = $('brailleTableType');
var updateTableType = function(setFocus) {
var currentTableType = localStorage['brailleTableType'] || 'brailleTable6';
if (currentTableType == 'brailleTable6') {
select6.removeAttribute('aria-hidden');
select6.setAttribute('tabIndex', 0);
select6.style.display = 'block';
if (setFocus) {
select6.focus();
}
select8.setAttribute('aria-hidden', 'true');
select8.setAttribute('tabIndex', -1);
select8.style.display = 'none';
localStorage['brailleTable'] = localStorage['brailleTable6'];
localStorage['brailleTableType'] = 'brailleTable6';
tableTypeButton.textContent =
cvox.ChromeVox.msgs.getMsg('options_braille_table_type_6');
} else {
select6.setAttribute('aria-hidden', 'true');
select6.setAttribute('tabIndex', -1);
select6.style.display = 'none';
select8.removeAttribute('aria-hidden');
select8.setAttribute('tabIndex', 0);
select8.style.display = 'block';
if (setFocus) {
select8.focus();
}
localStorage['brailleTable'] = localStorage['brailleTable8'];
localStorage['brailleTableType'] = 'brailleTable8';
tableTypeButton.textContent =
cvox.ChromeVox.msgs.getMsg('options_braille_table_type_8');
}
var braille = chrome.extension.getBackgroundPage().braille;
braille.refreshTranslator();
};
updateTableType(false);
tableTypeButton.addEventListener('click', function(evt) {
var 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.
*/
cvox.OptionsPage.setValue = function(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;
}
};
/**
* 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.
*/
cvox.OptionsPage.eventListener = function(event) {
window.setTimeout(function() {
var target = event.target;
if (target.classList.contains('pref')) {
if (target.tagName == 'INPUT' && target.type == 'checkbox') {
cvox.OptionsPage.prefs.setPref(target.name, target.checked);
} else if (target.tagName == 'INPUT' && target.type == 'radio') {
var key = target.name;
var elements = document.querySelectorAll('*[name="' + key + '"]');
for (var i = 0; i < elements.length; i++) {
if (elements[i].checked) {
cvox.OptionsPage.prefs.setPref(target.name, elements[i].value);
}
}
}
} else if (target.classList.contains('key')) {
var keySeq = cvox.KeySequence.fromStr(target.value);
var success = false;
if (target.id == 'cvoxKey') {
cvox.OptionsPage.prefs.setPref(target.id, target.value);
cvox.OptionsPage.prefs.sendPrefsToAllTabs(true, true);
success = true;
} else {
success =
cvox.OptionsPage.prefs.setKey(target.id, keySeq);
// TODO(dtseng): Don't surface conflicts until we have a better
// workflow.
}
}
}, 0);
return true;
};
/**
* Refreshes all dynamic content on the page.
This includes all key related information.
*/
cvox.OptionsPage.reset = function() {
var selectKeyMap = $('cvox_keymaps');
var id = selectKeyMap.options[selectKeyMap.selectedIndex].id;
var msgs = cvox.ChromeVox.msgs;
var announce = cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id ?
msgs.getMsg('keymap_reset', [msgs.getMsg(id)]) :
msgs.getMsg('keymap_switch', [msgs.getMsg(id)]);
cvox.OptionsPage.updateStatus_(announce);
cvox.OptionsPage.prefs.switchToKeyMap(id);
$('keysContainer').innerHTML = '';
cvox.OptionsPage.addKeys();
cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document);
};
/**
* Updates the status live region.
* @param {string} status The new status.
* @private
*/
cvox.OptionsPage.updateStatus_ = function(status) {
$('status').innerText = status;
};
/**
* Hides all elements not matching the current platform.
*/
cvox.OptionsPage.hidePlatformSpecifics = function() {
if (!cvox.ChromeVox.isChromeOS) {
var elements = document.body.querySelectorAll('.chromeos');
for (var i = 0, el; el = elements[i]; i++) {
el.setAttribute('aria-hidden', 'true');
el.style.display = 'none';
}
}
};
/**
* Calls a {@code cvox.TtsInterface.speak} method in the background page to
* speak an utterance. See that method for further details.
* @param {string} textString The string of text to be spoken.
* @param {cvox.QueueMode} queueMode The queue mode to use.
* @param {Object=} properties Speech properties to use for this utterance.
*/
cvox.OptionsPage.speak = function(textString, queueMode, properties) {
var speak =
/** @type Function} */ (chrome.extension.getBackgroundPage()['speak']);
speak.apply(null, arguments);
};
document.addEventListener('DOMContentLoaded', function() {
cvox.OptionsPage.init();
}, false);