1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * @fileoverview ChromeVox options page.
10 goog.provide('cvox.OptionsPage');
12 goog.require('cvox.BrailleBackground');
13 goog.require('cvox.BrailleTable');
14 goog.require('cvox.ChromeEarcons');
15 goog.require('cvox.ChromeHost');
16 goog.require('cvox.ChromeTts');
17 goog.require('cvox.ChromeVox');
18 goog.require('cvox.ChromeVoxPrefs');
19 goog.require('cvox.CommandStore');
20 goog.require('cvox.ExtensionBridge');
21 goog.require('cvox.HostFactory');
22 goog.require('cvox.KeyMap');
23 goog.require('cvox.KeySequence');
24 goog.require('cvox.Msgs');
25 goog.require('cvox.PlatformFilter');
26 goog.require('cvox.PlatformUtil');
29 * This object is exported by the main background page.
35 * Class to manage the options page.
38 cvox.OptionsPage = function() {
42 * The ChromeVoxPrefs object.
43 * @type {cvox.ChromeVoxPrefs}
45 cvox.OptionsPage.prefs;
49 * A mapping from keycodes to their human readable text equivalents.
50 * This is initialized in cvox.OptionsPage.init for internationalization.
51 * @type {Object.<string, string>}
53 cvox.OptionsPage.KEYCODE_TO_TEXT = {
57 * A mapping from human readable text to keycode values.
58 * This is initialized in cvox.OptionsPage.init for internationalization.
59 * @type {Object.<string, string>}
61 cvox.OptionsPage.TEXT_TO_KEYCODE = {
65 * Initialize the options page by setting the current value of all prefs,
66 * building the key bindings table, and adding event listeners.
67 * @suppress {missingProperties} Property prefs never defined on Window
69 cvox.OptionsPage.init = function() {
70 cvox.ChromeVox.msgs = new cvox.Msgs();
72 cvox.OptionsPage.prefs = chrome.extension.getBackgroundPage().prefs;
73 cvox.OptionsPage.populateKeyMapSelect();
74 cvox.OptionsPage.addKeys();
75 cvox.OptionsPage.populateVoicesSelect();
76 cvox.BrailleTable.getAll(function(tables) {
77 /** @type {!Array.<cvox.BrailleTable.Table>} */
78 cvox.OptionsPage.brailleTables = tables;
79 cvox.OptionsPage.populateBrailleTablesSelect();
82 cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document);
83 cvox.OptionsPage.hidePlatformSpecifics();
85 cvox.OptionsPage.update();
87 document.addEventListener('change', cvox.OptionsPage.eventListener, false);
88 document.addEventListener('click', cvox.OptionsPage.eventListener, false);
89 document.addEventListener('keydown', cvox.OptionsPage.eventListener, false);
91 cvox.ExtensionBridge.addMessageListener(function(message) {
92 if (message['keyBindings'] || message['prefs']) {
93 cvox.OptionsPage.update();
97 $('selectKeys').addEventListener(
98 'click', cvox.OptionsPage.reset, false);
100 if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) {
101 $('version').textContent =
102 chrome.app.getDetails().version;
107 * Update the value of controls to match the current preferences.
108 * This happens if the user presses a key in a tab that changes a
111 cvox.OptionsPage.update = function() {
112 var prefs = cvox.OptionsPage.prefs.getPrefs();
113 for (var key in prefs) {
114 // TODO(rshearer): 'active' is a pref, but there's no place in the
115 // options page to specify whether you want ChromeVox active.
116 var elements = document.querySelectorAll('*[name="' + key + '"]');
117 for (var i = 0; i < elements.length; i++) {
118 cvox.OptionsPage.setValue(elements[i], prefs[key]);
124 * Populate the keymap select element with stored keymaps
126 cvox.OptionsPage.populateKeyMapSelect = function() {
127 var select = $('cvox_keymaps');
128 for (var id in cvox.KeyMap.AVAILABLE_MAP_INFO) {
129 var info = cvox.KeyMap.AVAILABLE_MAP_INFO[id];
130 var option = document.createElement('option');
132 option.className = 'i18n';
133 option.setAttribute('msgid', id);
134 if (cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id) {
135 option.setAttribute('selected', '');
137 select.appendChild(option);
140 select.addEventListener('change', cvox.OptionsPage.reset, true);
144 * Add the input elements for the key bindings to the container element
145 * in the page. They're sorted in order of description.
147 cvox.OptionsPage.addKeys = function() {
148 var container = $('keysContainer');
149 var keyMap = cvox.OptionsPage.prefs.getKeyMap();
151 cvox.OptionsPage.prevTime = new Date().getTime();
152 cvox.OptionsPage.keyCount = 0;
153 container.addEventListener('keypress', goog.bind(function(evt) {
154 if (evt.target.id == 'cvoxKey') {
158 var currentTime = new Date().getTime();
159 if (currentTime - this.prevTime > 1000 || this.keyCount > 2) {
160 if (document.activeElement.id == 'toggleKeyPrefix') {
161 this.keySequence = new cvox.KeySequence(evt, false);
162 this.keySequence.keys['ctrlKey'][0] = true;
164 this.keySequence = new cvox.KeySequence(evt, true);
169 this.keySequence.addKeyEvent(evt);
172 var keySeqStr = cvox.KeyUtil.keySequenceToString(this.keySequence, true);
173 var announce = keySeqStr.replace(/\+/g,
174 ' ' + cvox.ChromeVox.msgs.getMsg('then') + ' ');
175 announce = announce.replace(/>/g,
176 ' ' + cvox.ChromeVox.msgs.getMsg('followed_by') + ' ');
177 announce = announce.replace('Cvox',
178 ' ' + cvox.ChromeVox.msgs.getMsg('modifier_key') + ' ');
180 // TODO(dtseng): Only basic conflict detection; it does not speak the
181 // conflicting command. Nor does it detect prefix conflicts like Cvox+L vs
183 if (cvox.OptionsPage.prefs.setKey(document.activeElement.id,
185 document.activeElement.value = keySeqStr;
187 announce = cvox.ChromeVox.msgs.getMsg('key_conflict', [announce]);
189 cvox.OptionsPage.speak(announce);
190 this.prevTime = currentTime;
192 evt.preventDefault();
193 evt.stopPropagation();
194 }, cvox.OptionsPage), true);
196 var categories = cvox.CommandStore.categories();
197 for (var i = 0; i < categories.length; i++) {
198 // Braille bindings can't be customized, so don't include them.
199 if (categories[i] == 'braille') {
202 var headerElement = document.createElement('h3');
203 headerElement.className = 'i18n';
204 headerElement.setAttribute('msgid', categories[i]);
205 headerElement.id = categories[i];
206 container.appendChild(headerElement);
207 var commands = cvox.CommandStore.commandsForCategory(categories[i]);
208 for (var j = 0; j < commands.length; j++) {
209 var command = commands[j];
210 // TODO: Someday we may want to have more than one key
211 // mapped to a command, so we'll need to figure out how to display
212 // that. For now, just take the first key.
213 var keySeqObj = keyMap.keyForCommand(command)[0];
215 // Explicitly skip toggleChromeVox in ChromeOS.
216 if (command == 'toggleChromeVox' &&
217 cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.CHROMEOS)) {
221 var inputElement = document.createElement('input');
222 inputElement.type = 'text';
223 inputElement.className = 'key active-key';
224 inputElement.id = command;
227 if (keySeqObj != null) {
228 displayedCombo = cvox.KeyUtil.keySequenceToString(keySeqObj, true);
232 inputElement.value = displayedCombo;
234 // Don't allow the user to change the sticky mode or stop speaking key.
235 if (command == 'toggleStickyMode' || command == 'stopSpeech') {
236 inputElement.disabled = true;
238 var message = cvox.CommandStore.messageForCommand(command);
240 // TODO(dtseng): missing message id's.
244 var labelElement = document.createElement('label');
245 labelElement.className = 'i18n';
246 labelElement.setAttribute('msgid', message);
247 labelElement.setAttribute('for', inputElement.id);
249 var divElement = document.createElement('div');
250 divElement.className = 'key-container';
251 container.appendChild(divElement);
252 divElement.appendChild(inputElement);
253 divElement.appendChild(labelElement);
255 var brElement = document.createElement('br');
256 container.appendChild(brElement);
259 if ($('cvoxKey') == null) {
260 // Add the cvox key field
261 var inputElement = document.createElement('input');
262 inputElement.type = 'text';
263 inputElement.className = 'key';
264 inputElement.id = 'cvoxKey';
266 var labelElement = document.createElement('label');
267 labelElement.className = 'i18n';
268 labelElement.setAttribute('msgid', 'options_cvox_modifier_key');
269 labelElement.setAttribute('for', 'cvoxKey');
271 var modifierSectionSibling =
272 $('modifier_keys').nextSibling;
273 var modifierSectionParent = modifierSectionSibling.parentNode;
274 modifierSectionParent.insertBefore(labelElement, modifierSectionSibling);
275 modifierSectionParent.insertBefore(inputElement, labelElement);
276 var cvoxKey = $('cvoxKey');
277 cvoxKey.value = localStorage['cvoxKey'];
279 cvoxKey.addEventListener('keydown', function(evt) {
280 if (!this.modifierSeq_) {
281 this.modifierCount_ = 0;
282 this.modifierSeq_ = new cvox.KeySequence(evt, false);
284 this.modifierSeq_.addKeyEvent(evt);
287 // Never allow non-modified keys.
288 if (!this.modifierSeq_.isAnyModifierActive()) {
289 // Indicate error and instructions excluding tab.
290 if (evt.keyCode != 9) {
291 cvox.OptionsPage.speak(
292 cvox.ChromeVox.msgs.getMsg('modifier_entry_error'), 0, {});
294 this.modifierSeq_ = null;
296 this.modifierCount_++;
299 // Don't trap tab or shift.
300 if (!evt.shiftKey && evt.keyCode != 9) {
301 evt.preventDefault();
302 evt.stopPropagation();
306 cvoxKey.addEventListener('keyup', function(evt) {
307 if (this.modifierSeq_) {
308 this.modifierCount_--;
310 if (this.modifierCount_ == 0) {
312 cvox.KeyUtil.keySequenceToString(this.modifierSeq_, true, true);
313 evt.target.value = modifierStr;
314 cvox.OptionsPage.speak(
315 cvox.ChromeVox.msgs.getMsg('modifier_entry_set', [modifierStr]));
316 localStorage['cvoxKey'] = modifierStr;
317 this.modifierSeq_ = null;
319 evt.preventDefault();
320 evt.stopPropagation();
327 * Populates the voices select with options.
329 cvox.OptionsPage.populateVoicesSelect = function() {
330 var select = $('voices');
332 function setVoiceList() {
333 select.innerHTML = '';
334 chrome.tts.getVoices(function(voices) {
335 voices.forEach(function(voice) {
336 var option = document.createElement('option');
337 option.voiceName = voice.voiceName || '';
338 option.innerText = option.voiceName;
339 chrome.storage.local.get('voiceName', function(items) {
340 if (items.voiceName == voice.voiceName) {
341 option.setAttribute('selected', '');
349 window.speechSynthesis.onvoiceschanged = setVoiceList.bind(this);
352 select.addEventListener('change', function(evt) {
353 var selIndex = select.selectedIndex;
354 var sel = select.options[selIndex];
355 chrome.storage.local.set({voiceName: sel.voiceName});
360 * Populates the braille select control.
361 * @this {cvox.OptionsPage}
363 cvox.OptionsPage.populateBrailleTablesSelect = function() {
364 if (!cvox.ChromeVox.isChromeOS) {
367 var tables = cvox.OptionsPage.brailleTables;
368 var populateSelect = function(node, dots) {
369 var activeTable = localStorage[node.id] || localStorage['brailleTable'];
370 // Gather the display names and sort them according to locale.
372 for (var i = 0, table; table = tables[i]; i++) {
373 if (table.dots !== dots) {
376 items.push({id: table.id,
377 name: cvox.BrailleTable.getDisplayName(table)});
379 items.sort(function(a, b) { return a.name.localeCompare(b.name);});
380 for (var i = 0, item; item = items[i]; ++i) {
381 var elem = document.createElement('option');
383 elem.textContent = item.name;
384 if (item.id == activeTable) {
385 elem.setAttribute('selected', '');
387 node.appendChild(elem);
390 var select6 = $('brailleTable6');
391 var select8 = $('brailleTable8');
392 populateSelect(select6, '6');
393 populateSelect(select8, '8');
395 var handleBrailleSelect = function(node) {
396 return function(evt) {
397 var selIndex = node.selectedIndex;
398 var sel = node.options[selIndex];
399 localStorage['brailleTable'] = sel.id;
400 localStorage[node.id] = sel.id;
401 /** @type {cvox.BrailleBackground} */
402 var braille = chrome.extension.getBackgroundPage().braille;
403 braille.refreshTranslator();
407 select6.addEventListener('change', handleBrailleSelect(select6), true);
408 select8.addEventListener('change', handleBrailleSelect(select8), true);
410 var tableTypeButton = $('brailleTableType');
411 var updateTableType = function(setFocus) {
412 var currentTableType = localStorage['brailleTableType'] || 'brailleTable6';
413 if (currentTableType == 'brailleTable6') {
414 select6.removeAttribute('aria-hidden');
415 select6.setAttribute('tabIndex', 0);
416 select6.style.display = 'block';
420 select8.setAttribute('aria-hidden', 'true');
421 select8.setAttribute('tabIndex', -1);
422 select8.style.display = 'none';
423 localStorage['brailleTable'] = localStorage['brailleTable6'];
424 localStorage['brailleTableType'] = 'brailleTable6';
425 tableTypeButton.textContent =
426 cvox.ChromeVox.msgs.getMsg('options_braille_table_type_6');
428 select6.setAttribute('aria-hidden', 'true');
429 select6.setAttribute('tabIndex', -1);
430 select6.style.display = 'none';
431 select8.removeAttribute('aria-hidden');
432 select8.setAttribute('tabIndex', 0);
433 select8.style.display = 'block';
437 localStorage['brailleTable'] = localStorage['brailleTable8'];
438 localStorage['brailleTableType'] = 'brailleTable8';
439 tableTypeButton.textContent =
440 cvox.ChromeVox.msgs.getMsg('options_braille_table_type_8');
442 var braille = chrome.extension.getBackgroundPage().braille;
443 braille.refreshTranslator();
445 updateTableType(false);
447 tableTypeButton.addEventListener('click', function(evt) {
448 var oldTableType = localStorage['brailleTableType'];
449 localStorage['brailleTableType'] =
450 oldTableType == 'brailleTable6' ? 'brailleTable8' : 'brailleTable6';
451 updateTableType(true);
456 * Set the html element for a preference to match the given value.
457 * @param {Element} element The HTML control.
458 * @param {string} value The new value.
460 cvox.OptionsPage.setValue = function(element, value) {
461 if (element.tagName == 'INPUT' && element.type == 'checkbox') {
462 element.checked = (value == 'true');
463 } else if (element.tagName == 'INPUT' && element.type == 'radio') {
464 element.checked = (String(element.value) == value);
466 element.value = value;
471 * Event listener, called when an event occurs in the page that might
472 * affect one of the preference controls.
473 * @param {Event} event The event.
474 * @return {boolean} True if the default action should occur.
476 cvox.OptionsPage.eventListener = function(event) {
477 window.setTimeout(function() {
478 var target = event.target;
479 if (target.classList.contains('pref')) {
480 if (target.tagName == 'INPUT' && target.type == 'checkbox') {
481 cvox.OptionsPage.prefs.setPref(target.name, target.checked);
482 } else if (target.tagName == 'INPUT' && target.type == 'radio') {
483 var key = target.name;
484 var elements = document.querySelectorAll('*[name="' + key + '"]');
485 for (var i = 0; i < elements.length; i++) {
486 if (elements[i].checked) {
487 cvox.OptionsPage.prefs.setPref(target.name, elements[i].value);
491 } else if (target.classList.contains('key')) {
492 var keySeq = cvox.KeySequence.fromStr(target.value);
494 if (target.id == 'cvoxKey') {
495 cvox.OptionsPage.prefs.setPref(target.id, target.value);
496 cvox.OptionsPage.prefs.sendPrefsToAllTabs(true, true);
500 cvox.OptionsPage.prefs.setKey(target.id, keySeq);
502 // TODO(dtseng): Don't surface conflicts until we have a better
511 * Refreshes all dynamic content on the page.
512 This includes all key related information.
514 cvox.OptionsPage.reset = function() {
515 var selectKeyMap = $('cvox_keymaps');
516 var id = selectKeyMap.options[selectKeyMap.selectedIndex].id;
518 var msgs = cvox.ChromeVox.msgs;
519 var announce = cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id ?
520 msgs.getMsg('keymap_reset', [msgs.getMsg(id)]) :
521 msgs.getMsg('keymap_switch', [msgs.getMsg(id)]);
522 cvox.OptionsPage.updateStatus_(announce);
524 cvox.OptionsPage.prefs.switchToKeyMap(id);
525 $('keysContainer').innerHTML = '';
526 cvox.OptionsPage.addKeys();
527 cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document);
531 * Updates the status live region.
532 * @param {string} status The new status.
535 cvox.OptionsPage.updateStatus_ = function(status) {
536 $('status').innerText = status;
541 * Hides all elements not matching the current platform.
543 cvox.OptionsPage.hidePlatformSpecifics = function() {
544 if (!cvox.ChromeVox.isChromeOS) {
545 var elements = document.body.querySelectorAll('.chromeos');
546 for (var i = 0, el; el = elements[i]; i++) {
547 el.setAttribute('aria-hidden', 'true');
548 el.style.display = 'none';
555 * Calls a {@code cvox.TtsInterface.speak} method in the background page to
556 * speak an utterance. See that method for further details.
557 * @param {string} textString The string of text to be spoken.
558 * @param {number=} queueMode The queue mode to use.
559 * @param {Object=} properties Speech properties to use for this utterance.
561 cvox.OptionsPage.speak = function(textString, queueMode, properties) {
563 /** @type Function} */ (chrome.extension.getBackgroundPage()['speak']);
564 speak.apply(null, arguments);
567 document.addEventListener('DOMContentLoaded', function() {
568 cvox.OptionsPage.init();