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