options.js revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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. 4 5/** 6 * @fileoverview ChromeVox options page. 7 * 8 */ 9 10goog.provide('cvox.OptionsPage'); 11 12goog.require('cvox.BrailleBackground'); 13goog.require('cvox.BrailleTable'); 14goog.require('cvox.ChromeEarcons'); 15goog.require('cvox.ChromeHost'); 16goog.require('cvox.ChromeTts'); 17goog.require('cvox.ChromeVox'); 18goog.require('cvox.ChromeVoxPrefs'); 19goog.require('cvox.CommandStore'); 20goog.require('cvox.ExtensionBridge'); 21goog.require('cvox.HostFactory'); 22goog.require('cvox.KeyMap'); 23goog.require('cvox.KeySequence'); 24goog.require('cvox.Msgs'); 25goog.require('cvox.PlatformFilter'); 26goog.require('cvox.PlatformUtil'); 27 28/** 29 * This object is exported by the main background page. 30 */ 31window.braille; 32 33 34/** 35 * Class to manage the options page. 36 * @constructor 37 */ 38cvox.OptionsPage = function() { 39}; 40 41/** 42 * The ChromeVoxPrefs object. 43 * @type {cvox.ChromeVoxPrefs} 44 */ 45cvox.OptionsPage.prefs; 46 47 48/** 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>} 52 */ 53cvox.OptionsPage.KEYCODE_TO_TEXT = { 54}; 55 56/** 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>} 60 */ 61cvox.OptionsPage.TEXT_TO_KEYCODE = { 62}; 63 64/** 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 68 */ 69cvox.OptionsPage.init = function() { 70 cvox.ChromeVox.msgs = new cvox.Msgs(); 71 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(); 80 }); 81 82 cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document); 83 cvox.OptionsPage.hidePlatformSpecifics(); 84 85 cvox.OptionsPage.update(); 86 87 document.addEventListener('change', cvox.OptionsPage.eventListener, false); 88 document.addEventListener('click', cvox.OptionsPage.eventListener, false); 89 document.addEventListener('keydown', cvox.OptionsPage.eventListener, false); 90 91 cvox.ExtensionBridge.addMessageListener(function(message) { 92 if (message['keyBindings'] || message['prefs']) { 93 cvox.OptionsPage.update(); 94 } 95 }); 96 97 $('selectKeys').addEventListener( 98 'click', cvox.OptionsPage.reset, false); 99 100 if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) { 101 $('version').textContent = 102 chrome.app.getDetails().version; 103 } 104}; 105 106/** 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 109 * pref. 110 */ 111cvox.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]); 119 } 120 } 121}; 122 123/** 124 * Populate the keymap select element with stored keymaps 125 */ 126cvox.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'); 131 option.id = id; 132 option.className = 'i18n'; 133 option.setAttribute('msgid', id); 134 if (cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id) { 135 option.setAttribute('selected', ''); 136 } 137 select.appendChild(option); 138 } 139 140 select.addEventListener('change', cvox.OptionsPage.reset, true); 141}; 142 143/** 144 * Add the input elements for the key bindings to the container element 145 * in the page. They're sorted in order of description. 146 */ 147cvox.OptionsPage.addKeys = function() { 148 var container = $('keysContainer'); 149 var keyMap = cvox.OptionsPage.prefs.getKeyMap(); 150 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') { 155 return; 156 } 157 this.keyCount++; 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; 163 } else { 164 this.keySequence = new cvox.KeySequence(evt, true); 165 } 166 167 this.keyCount = 1; 168 } else { 169 this.keySequence.addKeyEvent(evt); 170 } 171 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') + ' '); 179 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 182 // Cvox+L>L. 183 if (cvox.OptionsPage.prefs.setKey(document.activeElement.id, 184 this.keySequence)) { 185 document.activeElement.value = keySeqStr; 186 } else { 187 announce = cvox.ChromeVox.msgs.getMsg('key_conflict', [announce]); 188 } 189 cvox.OptionsPage.speak(announce); 190 this.prevTime = currentTime; 191 192 evt.preventDefault(); 193 evt.stopPropagation(); 194 }, cvox.OptionsPage), true); 195 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') { 200 continue; 201 } 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]; 214 215 // Explicitly skip toggleChromeVox in ChromeOS. 216 if (command == 'toggleChromeVox' && 217 cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.CHROMEOS)) { 218 continue; 219 } 220 221 var inputElement = document.createElement('input'); 222 inputElement.type = 'text'; 223 inputElement.className = 'key active-key'; 224 inputElement.id = command; 225 226 var displayedCombo; 227 if (keySeqObj != null) { 228 displayedCombo = cvox.KeyUtil.keySequenceToString(keySeqObj, true); 229 } else { 230 displayedCombo = ''; 231 } 232 inputElement.value = displayedCombo; 233 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; 237 } 238 var message = cvox.CommandStore.messageForCommand(command); 239 if (!message) { 240 // TODO(dtseng): missing message id's. 241 message = command; 242 } 243 244 var labelElement = document.createElement('label'); 245 labelElement.className = 'i18n'; 246 labelElement.setAttribute('msgid', message); 247 labelElement.setAttribute('for', inputElement.id); 248 249 var divElement = document.createElement('div'); 250 divElement.className = 'key-container'; 251 container.appendChild(divElement); 252 divElement.appendChild(inputElement); 253 divElement.appendChild(labelElement); 254 } 255 var brElement = document.createElement('br'); 256 container.appendChild(brElement); 257 } 258 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'; 265 266 var labelElement = document.createElement('label'); 267 labelElement.className = 'i18n'; 268 labelElement.setAttribute('msgid', 'options_cvox_modifier_key'); 269 labelElement.setAttribute('for', 'cvoxKey'); 270 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']; 278 279 cvoxKey.addEventListener('keydown', function(evt) { 280 if (!this.modifierSeq_) { 281 this.modifierCount_ = 0; 282 this.modifierSeq_ = new cvox.KeySequence(evt, false); 283 } else { 284 this.modifierSeq_.addKeyEvent(evt); 285 } 286 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, {}); 293 } 294 this.modifierSeq_ = null; 295 } else { 296 this.modifierCount_++; 297 } 298 299 // Don't trap tab or shift. 300 if (!evt.shiftKey && evt.keyCode != 9) { 301 evt.preventDefault(); 302 evt.stopPropagation(); 303 } 304 }, true); 305 306 cvoxKey.addEventListener('keyup', function(evt) { 307 if (this.modifierSeq_) { 308 this.modifierCount_--; 309 310 if (this.modifierCount_ == 0) { 311 var modifierStr = 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; 318 } 319 evt.preventDefault(); 320 evt.stopPropagation(); 321 } 322 }, true); 323 } 324}; 325 326/** 327 * Populates the voices select with options. 328 */ 329cvox.OptionsPage.populateVoicesSelect = function() { 330 var select = $('voices'); 331 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', ''); 342 } 343 }); 344 select.add(option); 345 }); 346 }); 347 } 348 349 window.speechSynthesis.onvoiceschanged = setVoiceList.bind(this); 350 setVoiceList(); 351 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}); 356 }, true); 357}; 358 359/** 360 * Populates the braille select control. 361 * @this {cvox.OptionsPage} 362 */ 363cvox.OptionsPage.populateBrailleTablesSelect = function() { 364 if (!cvox.ChromeVox.isChromeOS) { 365 return; 366 } 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. 371 var items = []; 372 for (var i = 0, table; table = tables[i]; i++) { 373 if (table.dots !== dots) { 374 continue; 375 } 376 items.push({id: table.id, 377 name: cvox.BrailleTable.getDisplayName(table)}); 378 } 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'); 382 elem.id = item.id; 383 elem.textContent = item.name; 384 if (item.id == activeTable) { 385 elem.setAttribute('selected', ''); 386 } 387 node.appendChild(elem); 388 } 389 }; 390 var select6 = $('brailleTable6'); 391 var select8 = $('brailleTable8'); 392 populateSelect(select6, '6'); 393 populateSelect(select8, '8'); 394 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(); 404 }; 405 }; 406 407 select6.addEventListener('change', handleBrailleSelect(select6), true); 408 select8.addEventListener('change', handleBrailleSelect(select8), true); 409 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'; 417 if (setFocus) { 418 select6.focus(); 419 } 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'); 427 } else { 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'; 434 if (setFocus) { 435 select8.focus(); 436 } 437 localStorage['brailleTable'] = localStorage['brailleTable8']; 438 localStorage['brailleTableType'] = 'brailleTable8'; 439 tableTypeButton.textContent = 440 cvox.ChromeVox.msgs.getMsg('options_braille_table_type_8'); 441 } 442 var braille = chrome.extension.getBackgroundPage().braille; 443 braille.refreshTranslator(); 444 }; 445 updateTableType(false); 446 447 tableTypeButton.addEventListener('click', function(evt) { 448 var oldTableType = localStorage['brailleTableType']; 449 localStorage['brailleTableType'] = 450 oldTableType == 'brailleTable6' ? 'brailleTable8' : 'brailleTable6'; 451 updateTableType(true); 452 }, true); 453}; 454 455/** 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. 459 */ 460cvox.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); 465 } else { 466 element.value = value; 467 } 468}; 469 470/** 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. 475 */ 476cvox.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); 488 } 489 } 490 } 491 } else if (target.classList.contains('key')) { 492 var keySeq = cvox.KeySequence.fromStr(target.value); 493 var success = false; 494 if (target.id == 'cvoxKey') { 495 cvox.OptionsPage.prefs.setPref(target.id, target.value); 496 cvox.OptionsPage.prefs.sendPrefsToAllTabs(true, true); 497 success = true; 498 } else { 499 success = 500 cvox.OptionsPage.prefs.setKey(target.id, keySeq); 501 502 // TODO(dtseng): Don't surface conflicts until we have a better 503 // workflow. 504 } 505 } 506 }, 0); 507 return true; 508}; 509 510/** 511 * Refreshes all dynamic content on the page. 512This includes all key related information. 513 */ 514cvox.OptionsPage.reset = function() { 515 var selectKeyMap = $('cvox_keymaps'); 516 var id = selectKeyMap.options[selectKeyMap.selectedIndex].id; 517 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); 523 524 cvox.OptionsPage.prefs.switchToKeyMap(id); 525 $('keysContainer').innerHTML = ''; 526 cvox.OptionsPage.addKeys(); 527 cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document); 528}; 529 530/** 531 * Updates the status live region. 532 * @param {string} status The new status. 533 * @private 534 */ 535cvox.OptionsPage.updateStatus_ = function(status) { 536 $('status').innerText = status; 537}; 538 539 540/** 541 * Hides all elements not matching the current platform. 542 */ 543cvox.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'; 549 } 550 } 551}; 552 553 554/** 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. 560 */ 561cvox.OptionsPage.speak = function(textString, queueMode, properties) { 562 var speak = 563 /** @type Function} */ (chrome.extension.getBackgroundPage()['speak']); 564 speak.apply(null, arguments); 565}; 566 567document.addEventListener('DOMContentLoaded', function() { 568 cvox.OptionsPage.init(); 569}, false); 570