key_map.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/**
7 * @fileoverview This class provides a stable interface for initializing,
8 * querying, and modifying a ChromeVox key map.
9 *
10 * An instance contains an object-based bi-directional mapping from key binding
11 * to a function name of a user command (herein simply called a command).
12 * A caller is responsible for providing a JSON keymap (a simple Object key
13 * value structure), which has (key, command) key value pairs.
14 *
15 * Due to execution of user commands within the content script, the function
16 * name of the command is not explicitly checked within the background page via
17 * Closure. Any errors would only be caught at runtime.
18 *
19 * To retrieve static data about user commands, see both cvox.CommandStore and
20 * cvox.UserCommands.
21 */
22
23goog.provide('cvox.KeyMap');
24
25// TODO(dtseng): Only needed for sticky mode.
26goog.require('cvox.KeyUtil');
27goog.require('cvox.PlatformUtil');
28
29/**
30 * @param {Array.<Object.<string,
31 *         {command: string, sequence: cvox.KeySequence}>>}
32 * commandsAndKeySequences An array of pairs - KeySequences and commands.
33 * @constructor
34 */
35cvox.KeyMap = function(commandsAndKeySequences) {
36  /**
37   * An array of bindings - commands and KeySequences.
38   * @type {Array.<Object.<string,
39   *        {command: string, sequence: cvox.KeySequence}>>}
40   * @private
41   */
42  this.bindings_ = commandsAndKeySequences;
43
44  /**
45   * Maps a command to a key. This optimizes the process of searching for a
46   * key sequence when you already know the command.
47   * @type {Object.<string, cvox.KeySequence>}
48   * @private
49   */
50  this.commandToKey_ = {};
51  this.buildCommandToKey_();
52};
53
54
55/**
56 * Path to dir containing ChromeVox keymap json definitions.
57 * @type {string}
58 * @const
59 */
60cvox.KeyMap.KEYMAP_PATH = 'chromevox/background/keymaps/';
61
62
63/**
64 * An array of available key maps sorted by priority.
65 * (The first map is the default, the last is the least important).
66 * TODO(dtseng): Not really sure this belongs here, but it doesn't seem to be
67 * user configurable, so it doesn't make sense to json-stringify it.
68 * Should have class to siwtch among and manage multiple key maps.
69 * @type {Object.<string, Object.<string, string>>}
70 * @const
71 */
72cvox.KeyMap.AVAILABLE_MAP_INFO = {
73'keymap_classic': {
74    'file': 'classic_keymap.json'
75  },
76'keymap_flat': {
77    'file': 'flat_keymap.json'
78  },
79'keymap_experimental': {
80    'file': 'experimental.json'
81  }
82};
83
84
85/**
86 * The index of the default key map info in cvox.KeyMap.AVAIABLE_KEYMAP_INFO.
87 * @type {number}
88 * @const
89 */
90cvox.KeyMap.DEFAULT_KEYMAP = 0;
91
92
93/**
94 * The number of mappings in the keymap.
95 * @return {number} The number of mappings.
96 */
97cvox.KeyMap.prototype.length = function() {
98  return this.bindings_.length;
99};
100
101
102/**
103 * Returns a copy of all KeySequences in this map.
104 * @return {Array.<cvox.KeySequence>} Array of all keys.
105 */
106cvox.KeyMap.prototype.keys = function() {
107  return this.bindings_.map(function(binding) {
108    return binding.sequence;
109  });
110};
111
112
113/**
114 * Returns a collection of command, KeySequence bindings.
115 * @return {Array.<Object.<string, cvox.KeySequence>>} Array of all command,
116 * key bindings.
117 * @suppress {checkTypes} inconsistent return type
118 * found   : (Array.<(Object.<{command: string,
119 *                             sequence: (cvox.KeySequence|null)}>|null)>|null)
120 * required: (Array.<(Object.<(cvox.KeySequence|null)>|null)>|null)
121 */
122cvox.KeyMap.prototype.bindings = function() {
123  return this.bindings_;
124};
125
126
127/**
128 * This method is called when cvox.KeyMap instances are stringified via
129 * JSON.stringify.
130 * @return {string} The JSON representation of this instance.
131 */
132cvox.KeyMap.prototype.toJSON = function() {
133  return JSON.stringify({bindings: this.bindings_});
134};
135
136
137/**
138 * Writes to local storage.
139 */
140cvox.KeyMap.prototype.toLocalStorage = function() {
141  localStorage['keyBindings'] = this.toJSON();
142};
143
144
145/**
146 * Checks if this key map has a given binding.
147 * @param {string} command The command.
148 * @param {cvox.KeySequence} sequence The key sequence.
149 * @return {boolean} Whether the binding exists.
150 */
151cvox.KeyMap.prototype.hasBinding = function(command, sequence) {
152  if (this.commandToKey_ != null) {
153    return this.commandToKey_[command] == sequence;
154  } else {
155    for (var i = 0; i < this.bindings_.length; i++) {
156      var binding = this.bindings_[i];
157      if (binding.command == command && binding.sequence == sequence) {
158        return true;
159      }
160    }
161  }
162  return false;
163};
164
165
166/**
167 * Checks if this key map has a given command.
168 * @param {string} command The command to check.
169 * @return {boolean} Whether 'command' has a binding.
170 */
171cvox.KeyMap.prototype.hasCommand = function(command) {
172  if (this.commandToKey_ != null) {
173    return this.commandToKey_[command] != undefined;
174  } else {
175    for (var i = 0; i < this.bindings_.length; i++) {
176      var binding = this.bindings_[i];
177      if (binding.command == command) {
178        return true;
179      }
180    }
181  }
182  return false;
183};
184
185
186/**
187 * Checks if this key map has a given key.
188 * @param {cvox.KeySequence} key The key to check.
189 * @return {boolean} Whether 'key' has a binding.
190 */
191cvox.KeyMap.prototype.hasKey = function(key) {
192  for (var i = 0; i < this.bindings_.length; i++) {
193    var binding = this.bindings_[i];
194    if (binding.sequence.equals(key)) {
195      return true;
196    }
197  }
198  return false;
199};
200
201
202/**
203 * Gets a command given a key.
204 * @param {cvox.KeySequence} key The key to query.
205 * @return {?string} The command, if any.
206 */
207cvox.KeyMap.prototype.commandForKey = function(key) {
208  if (key != null) {
209    for (var i = 0; i < this.bindings_.length; i++) {
210      var binding = this.bindings_[i];
211      if (binding.sequence.equals(key)) {
212        return binding.command;
213      }
214    }
215  }
216  return null;
217};
218
219
220/**
221 * Gets a key given a command.
222 * @param {string} command The command to query.
223 * @return {!Array.<cvox.KeySequence>} The keys associated with that command,
224 * if any.
225 */
226cvox.KeyMap.prototype.keyForCommand = function(command) {
227  if (this.commandToKey_ != null) {
228    return [this.commandToKey_[command]];
229  } else {
230    var keySequenceArray = [];
231     for (var i = 0; i < this.bindings_.length; i++) {
232      var binding = this.bindings_[i];
233       if (binding.command == command) {
234         keySequenceArray.push(binding.sequence);
235       }
236     }
237  }
238  return (keySequenceArray.length > 0) ? keySequenceArray : [];
239};
240
241
242/**
243 * Merges an input map with this one. The merge preserves this instance's
244 * mappings. It only adds new bindings if there isn't one already.
245 * If either the incoming binding's command or key exist in this, it will be
246 * ignored.
247 * @param {!cvox.KeyMap} inputMap The map to merge with this.
248 * @return {boolean} True if there were no merge conflicts.
249 */
250cvox.KeyMap.prototype.merge = function(inputMap) {
251  var keys = inputMap.keys();
252  var cleanMerge = true;
253  for (var i = 0; i < keys.length; ++i) {
254    var key = keys[i];
255    var command = inputMap.commandForKey(key);
256    if (command == 'toggleStickyMode') {
257      // TODO(dtseng): More uglyness because of sticky key.
258      continue;
259    } else if (key && command &&
260               !this.hasKey(key) && !this.hasCommand(command)) {
261      this.bind_(command, key);
262    } else {
263      cleanMerge = false;
264    }
265  }
266  return cleanMerge;
267};
268
269
270/**
271 * Changes an existing key binding to a new key. If the key is already bound to
272 * a command, the rebind will fail.
273 * @param {string} command The command to set.
274 * @param {cvox.KeySequence} newKey The new key to assign it to.
275 * @return {boolean} Whether the rebinding succeeds.
276 */
277cvox.KeyMap.prototype.rebind = function(command, newKey) {
278  if (this.hasCommand(command) && !this.hasKey(newKey)) {
279    this.bind_(command, newKey);
280    return true;
281  }
282  return false;
283};
284
285
286/**
287 * Changes a key binding. Any existing bindings to the given key will be
288 * deleted. Use this.rebind to have non-overwrite behavior.
289 * @param {string} command The command to set.
290 * @param {cvox.KeySequence} newKey The new key to assign it to.
291 * @private
292 */
293cvox.KeyMap.prototype.bind_ = function(command, newKey) {
294  // TODO(dtseng): Need unit test to ensure command is valid for every *.json
295  // keymap.
296  var bound = false;
297  for (var i = 0; i < this.bindings_.length; i++) {
298    var binding = this.bindings_[i];
299    if (binding.command == command) {
300      // Replace the key with the new key.
301      delete binding.sequence;
302      binding.sequence = newKey;
303      if (this.commandToKey_ != null) {
304        this.commandToKey_[binding.command] = newKey;
305      }
306      bound = true;
307    }
308  }
309  if (!bound) {
310    var binding = {
311      'command': command,
312      'sequence': newKey
313    };
314    this.bindings_.push(binding);
315    this.commandToKey_[binding.command] = binding.sequence;
316  }
317};
318
319
320// TODO(dtseng): Move to a manager class.
321/**
322 * Convenience method for getting a default key map.
323 * @return {!cvox.KeyMap} The default key map.
324 */
325cvox.KeyMap.fromDefaults = function() {
326  return /** @type {!cvox.KeyMap} */ (
327    cvox.KeyMap.fromPath(cvox.KeyMap.KEYMAP_PATH +
328        cvox.KeyMap.AVAILABLE_MAP_INFO['keymap_classic'].file));
329};
330
331
332/**
333 * Convenience method for creating a key map based on a JSON (key, value) Object
334 * where the key is a literal keyboard string and value is a command string.
335 * @param {string} json The JSON.
336 * @return {cvox.KeyMap} The resulting object; null if unable to parse.
337 */
338cvox.KeyMap.fromJSON = function(json) {
339  try {
340    var commandsAndKeySequences =
341        /** @type {Array.<Object.<string,
342         *          {command: string, sequence: cvox.KeySequence}>>} */
343    (JSON.parse(json).bindings);
344    commandsAndKeySequences = commandsAndKeySequences.filter(function(value) {
345      return value.sequence.platformFilter === undefined ||
346          cvox.PlatformUtil.matchesPlatform(value.sequence.platformFilter);
347    });
348  } catch (e) {
349    return null;
350  }
351
352  // Validate the type of the commandsAndKeySequences array.
353  if (typeof(commandsAndKeySequences) != 'object') {
354    return null;
355  }
356  for (var i = 0; i < commandsAndKeySequences.length; i++) {
357    if (commandsAndKeySequences[i].command == undefined ||
358        commandsAndKeySequences[i].sequence == undefined) {
359      return null;
360    } else {
361      commandsAndKeySequences[i].sequence = /** @type {cvox.KeySequence} */
362        (cvox.KeySequence.deserialize(commandsAndKeySequences[i].sequence));
363    }
364  }
365  return new cvox.KeyMap(commandsAndKeySequences);
366};
367
368
369/**
370 * Convenience method for creating a map local storage.
371 * @return {cvox.KeyMap} A map that reads from local storage.
372 */
373cvox.KeyMap.fromLocalStorage = function() {
374  if (localStorage['keyBindings']) {
375    return cvox.KeyMap.fromJSON(localStorage['keyBindings']);
376  }
377  return null;
378};
379
380
381/**
382 * Convenience method for creating a cvox.KeyMap based on a path.
383 * Warning: you should only call this within a background page context.
384 * @param {string} path A valid path of the form
385 * chromevox/background/keymaps/*.json.
386 * @return {cvox.KeyMap} A valid KeyMap object; null on error.
387 */
388cvox.KeyMap.fromPath = function(path) {
389  return cvox.KeyMap.fromJSON(cvox.KeyMap.readJSON_(path));
390};
391
392
393/**
394 * Convenience method for getting a currently selected key map.
395 * @return {!cvox.KeyMap} The currently selected key map.
396 */
397cvox.KeyMap.fromCurrentKeyMap = function() {
398  var map = localStorage['currentKeyMap'];
399  if (map && cvox.KeyMap.AVAILABLE_MAP_INFO[map]) {
400    return /** @type {!cvox.KeyMap} */ (cvox.KeyMap.fromPath(
401        cvox.KeyMap.KEYMAP_PATH + cvox.KeyMap.AVAILABLE_MAP_INFO[map].file));
402  } else {
403    return cvox.KeyMap.fromDefaults();
404  }
405};
406
407
408/**
409 * Takes a path to a JSON file and returns a JSON Object.
410 * @param {string} path Contains the path to a JSON file.
411 * @return {string} JSON.
412 * @private
413 * @suppress {missingProperties}
414 */
415cvox.KeyMap.readJSON_ = function(path) {
416  var url = chrome.extension.getURL(path);
417  if (!url) {
418    throw 'Invalid path: ' + path;
419  }
420
421  var xhr = new XMLHttpRequest();
422  xhr.open('GET', url, false);
423  xhr.send();
424  return xhr.responseText;
425};
426
427
428/**
429 * Resets the default modifier keys.
430 * TODO(dtseng): Move elsewhere when we figure out our localStorage story.
431 */
432cvox.KeyMap.prototype.resetModifier = function() {
433  localStorage['cvoxKey'] = cvox.ChromeVox.modKeyStr;
434};
435
436
437/**
438 * Builds the map of commands to keys.
439 * @private
440 */
441cvox.KeyMap.prototype.buildCommandToKey_ = function() {
442  // TODO (dtseng): What about more than one sequence mapped to the same
443  // command?
444  for (var i = 0; i < this.bindings_.length; i++) {
445    var binding = this.bindings_[i];
446    if (this.commandToKey_[binding.command] != undefined) {
447      // There's at least two key sequences mapped to the same
448      // command. continue.
449      continue;
450    }
451    this.commandToKey_[binding.command] = binding.sequence;
452  }
453};
454