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