Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / chromevox / chromevox / background / options.js
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
10 goog.provide('cvox.OptionsPage');
11
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');
27
28 /**
29  * This object is exported by the main background page.
30  */
31 window.braille;
32
33
34 /**
35  * Class to manage the options page.
36  * @constructor
37  */
38 cvox.OptionsPage = function() {
39 };
40
41 /**
42  * The ChromeVoxPrefs object.
43  * @type {cvox.ChromeVoxPrefs}
44  */
45 cvox.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  */
53 cvox.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  */
61 cvox.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  */
69 cvox.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  */
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]);
119     }
120   }
121 };
122
123 /**
124  * Populate the keymap select element with stored keymaps
125  */
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');
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  */
147 cvox.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  */
329 cvox.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  */
363 cvox.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  */
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);
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  */
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);
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.
512 This includes all key related information.
513  */
514 cvox.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  */
535 cvox.OptionsPage.updateStatus_ = function(status) {
536   $('status').innerText = status;
537 };
538
539
540 /**
541  * Hides all elements not matching the current platform.
542  */
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';
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  */
561 cvox.OptionsPage.speak = function(textString, queueMode, properties) {
562   var speak =
563       /** @type Function} */ (chrome.extension.getBackgroundPage()['speak']);
564   speak.apply(null, arguments);
565 };
566
567 document.addEventListener('DOMContentLoaded', function() {
568   cvox.OptionsPage.init();
569 }, false);