options.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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.ChromeMsgs'); 17goog.require('cvox.ChromeTts'); 18goog.require('cvox.ChromeVox'); 19goog.require('cvox.ChromeVoxPrefs'); 20goog.require('cvox.CommandStore'); 21goog.require('cvox.ExtensionBridge'); 22goog.require('cvox.HostFactory'); 23goog.require('cvox.KeyMap'); 24goog.require('cvox.KeySequence'); 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 = cvox.HostFactory.getMsgs(); 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 document.getElementById('selectKeys').addEventListener( 98 'click', cvox.OptionsPage.reset, false); 99 100 if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) { 101 document.getElementById('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 = document.getElementById('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 = document.getElementById('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 (document.getElementById('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 document.getElementById('modifier_keys').nextSibling; 273 var modifierSectionParent = modifierSectionSibling.parentNode; 274 modifierSectionParent.insertBefore(labelElement, modifierSectionSibling); 275 modifierSectionParent.insertBefore(inputElement, labelElement); 276 var cvoxKey = document.getElementById('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 = document.getElementById('voices'); 331 chrome.tts.getVoices(function(voices) { 332 voices.forEach(function(voice) { 333 var option = document.createElement('option'); 334 option.voiceName = voice.voiceName || ''; 335 option.innerText = option.voiceName; 336 if (localStorage['voiceName'] == voice.voiceName) { 337 option.setAttribute('selected', ''); 338 } 339 select.add(option); 340 }); 341 }); 342 343 select.addEventListener('change', function(evt) { 344 var selIndex = select.selectedIndex; 345 var sel = select.options[selIndex]; 346 localStorage['voiceName'] = sel.voiceName; 347 }, true); 348}; 349 350/** 351 * Populates the braille select control. 352 * @this {cvox.OptionsPage} 353 */ 354cvox.OptionsPage.populateBrailleTablesSelect = function() { 355 if (!cvox.ChromeVox.isChromeOS) { 356 return; 357 } 358 var tables = cvox.OptionsPage.brailleTables; 359 var localeDict = JSON.parse(cvox.ChromeVox.msgs.getMsg('locale_dict')); 360 var populateSelect = function(node, dots) { 361 var localeCount = []; 362 var activeTable = localStorage[node.id] || localStorage['brailleTable']; 363 for (var i = 0, table; table = tables[i]; i++) { 364 if (table.dots !== dots) { 365 continue; 366 } 367 var item = document.createElement('option'); 368 item.id = table.id; 369 if (!localeCount[table.locale]) { 370 localeCount[table.locale] = 0; 371 } 372 localeCount[table.locale]++; 373 var grade = table.grade; 374 if (!grade) { 375 item.textContent = localeDict[table.locale]; 376 } else { 377 item.textContent = cvox.ChromeVox.msgs.getMsg( 378 'options_braille_locale_grade', 379 [localeDict[table.locale], grade]); 380 } 381 if (table.id == activeTable) { 382 item.setAttribute('selected', ''); 383 } 384 node.appendChild(item); 385 } 386 }; 387 var select6 = document.getElementById('brailleTable6'); 388 var select8 = document.getElementById('brailleTable8'); 389 populateSelect(select6, '6'); 390 populateSelect(select8, '8'); 391 392 var handleBrailleSelect = function(node) { 393 return function(evt) { 394 var selIndex = node.selectedIndex; 395 var sel = node.options[selIndex]; 396 localStorage['brailleTable'] = sel.id; 397 localStorage[node.id] = sel.id; 398 /** @type {cvox.BrailleBackground} */ 399 var braille = chrome.extension.getBackgroundPage().braille; 400 braille.refreshTranslator(); 401 }; 402 }; 403 404 select6.addEventListener('change', handleBrailleSelect(select6), true); 405 select8.addEventListener('change', handleBrailleSelect(select8), true); 406 407 var tableTypeButton = document.getElementById('brailleTableType'); 408 var updateTableType = function(setFocus) { 409 var currentTableType = localStorage['brailleTableType'] || 'brailleTable6'; 410 if (currentTableType == 'brailleTable6') { 411 select6.removeAttribute('aria-hidden'); 412 select6.setAttribute('tabIndex', 0); 413 select6.style.display = 'block'; 414 if (setFocus) { 415 select6.focus(); 416 } 417 select8.setAttribute('aria-hidden', 'true'); 418 select8.setAttribute('tabIndex', -1); 419 select8.style.display = 'none'; 420 localStorage['brailleTable'] = localStorage['brailleTable6']; 421 localStorage['brailleTableType'] = 'brailleTable6'; 422 tableTypeButton.textContent = 423 cvox.ChromeVox.msgs.getMsg('options_braille_table_type_6'); 424 } else { 425 select6.setAttribute('aria-hidden', 'true'); 426 select6.setAttribute('tabIndex', -1); 427 select6.style.display = 'none'; 428 select8.removeAttribute('aria-hidden'); 429 select8.setAttribute('tabIndex', 0); 430 select8.style.display = 'block'; 431 if (setFocus) { 432 select8.focus(); 433 } 434 localStorage['brailleTable'] = localStorage['brailleTable8']; 435 localStorage['brailleTableType'] = 'brailleTable8'; 436 tableTypeButton.textContent = 437 cvox.ChromeVox.msgs.getMsg('options_braille_table_type_8'); 438 } 439 var braille = chrome.extension.getBackgroundPage().braille; 440 braille.refreshTranslator(); 441 }; 442 updateTableType(false); 443 444 tableTypeButton.addEventListener('click', function(evt) { 445 var oldTableType = localStorage['brailleTableType']; 446 localStorage['brailleTableType'] = 447 oldTableType == 'brailleTable6' ? 'brailleTable8' : 'brailleTable6'; 448 updateTableType(true); 449 }, true); 450}; 451 452/** 453 * Set the html element for a preference to match the given value. 454 * @param {Element} element The HTML control. 455 * @param {string} value The new value. 456 */ 457cvox.OptionsPage.setValue = function(element, value) { 458 if (element.tagName == 'INPUT' && element.type == 'checkbox') { 459 element.checked = (value == 'true'); 460 } else if (element.tagName == 'INPUT' && element.type == 'radio') { 461 element.checked = (String(element.value) == value); 462 } else { 463 element.value = value; 464 } 465}; 466 467/** 468 * Event listener, called when an event occurs in the page that might 469 * affect one of the preference controls. 470 * @param {Event} event The event. 471 * @return {boolean} True if the default action should occur. 472 */ 473cvox.OptionsPage.eventListener = function(event) { 474 window.setTimeout(function() { 475 var target = event.target; 476 if (target.classList.contains('pref')) { 477 if (target.tagName == 'INPUT' && target.type == 'checkbox') { 478 cvox.OptionsPage.prefs.setPref(target.name, target.checked); 479 } else if (target.tagName == 'INPUT' && target.type == 'radio') { 480 var key = target.name; 481 var elements = document.querySelectorAll('*[name="' + key + '"]'); 482 for (var i = 0; i < elements.length; i++) { 483 if (elements[i].checked) { 484 cvox.OptionsPage.prefs.setPref(target.name, elements[i].value); 485 } 486 } 487 } 488 } else if (target.classList.contains('key')) { 489 var keySeq = cvox.KeySequence.fromStr(target.value); 490 var success = false; 491 if (target.id == 'cvoxKey') { 492 cvox.OptionsPage.prefs.setPref(target.id, target.value); 493 cvox.OptionsPage.prefs.sendPrefsToAllTabs(true, true); 494 success = true; 495 } else { 496 success = 497 cvox.OptionsPage.prefs.setKey(target.id, keySeq); 498 499 // TODO(dtseng): Don't surface conflicts until we have a better 500 // workflow. 501 } 502 } 503 }, 0); 504 return true; 505}; 506 507/** 508 * Refreshes all dynamic content on the page. 509This includes all key related information. 510 */ 511cvox.OptionsPage.reset = function() { 512 var selectKeyMap = document.getElementById('cvox_keymaps'); 513 var id = selectKeyMap.options[selectKeyMap.selectedIndex].id; 514 515 var msgs = cvox.ChromeVox.msgs; 516 var announce = cvox.OptionsPage.prefs.getPrefs()['currentKeyMap'] == id ? 517 msgs.getMsg('keymap_reset', [msgs.getMsg(id)]) : 518 msgs.getMsg('keymap_switch', [msgs.getMsg(id)]); 519 cvox.OptionsPage.updateStatus_(announce); 520 521 cvox.OptionsPage.prefs.switchToKeyMap(id); 522 document.getElementById('keysContainer').innerHTML = ''; 523 cvox.OptionsPage.addKeys(); 524 cvox.ChromeVox.msgs.addTranslatedMessagesToDom(document); 525}; 526 527/** 528 * Updates the status live region. 529 * @param {string} status The new status. 530 * @private 531 */ 532cvox.OptionsPage.updateStatus_ = function(status) { 533 document.getElementById('status').innerText = status; 534}; 535 536 537/** 538 * Hides all elements not matching the current platform. 539 */ 540cvox.OptionsPage.hidePlatformSpecifics = function() { 541 if (!cvox.ChromeVox.isChromeOS) { 542 var elements = document.body.querySelectorAll('.chromeos'); 543 for (var i = 0, el; el = elements[i]; i++) { 544 el.setAttribute('aria-hidden', 'true'); 545 el.style.display = 'none'; 546 } 547 } 548}; 549 550 551/** 552 * Calls a {@code cvox.TtsInterface.speak} method in the background page to 553 * speak an utterance. See that method for further details. 554 * @param {string} textString The string of text to be spoken. 555 * @param {number=} queueMode The queue mode to use. 556 * @param {Object=} properties Speech properties to use for this utterance. 557 */ 558cvox.OptionsPage.speak = function(textString, queueMode, properties) { 559 var speak = 560 /** @type Function} */ (chrome.extension.getBackgroundPage()['speak']); 561 speak.apply(null, arguments); 562}; 563 564document.addEventListener('DOMContentLoaded', function() { 565 cvox.OptionsPage.init(); 566}, false); 567