/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of Ajax.org B.V. nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ define(function(require, exports, module) { "use strict"; // If you're developing a new keymapping and want to get an idea what's going // on, then enable debugging. var DEBUG = false; function StateHandler(keymapping) { this.keymapping = this.$buildKeymappingRegex(keymapping); } StateHandler.prototype = { /* * Build the RegExp from the keymapping as RegExp can't stored directly * in the metadata JSON and as the RegExp used to match the keys/buffer * need to be adapted. */ $buildKeymappingRegex: function(keymapping) { for (var state in keymapping) { this.$buildBindingsRegex(keymapping[state]); } return keymapping; }, $buildBindingsRegex: function(bindings) { // Escape a given Regex string. bindings.forEach(function(binding) { if (binding.key) { binding.key = new RegExp('^' + binding.key + '$'); } else if (Array.isArray(binding.regex)) { if (!('key' in binding)) binding.key = new RegExp('^' + binding.regex[1] + '$'); binding.regex = new RegExp(binding.regex.join('') + '$'); } else if (binding.regex) { binding.regex = new RegExp(binding.regex + '$'); } }); }, $composeBuffer: function(data, hashId, key, e) { // Initialize the data object. if (data.state == null || data.buffer == null) { data.state = "start"; data.buffer = ""; } var keyArray = []; if (hashId & 1) keyArray.push("ctrl"); if (hashId & 8) keyArray.push("command"); if (hashId & 2) keyArray.push("option"); if (hashId & 4) keyArray.push("shift"); if (key) keyArray.push(key); var symbolicName = keyArray.join("-"); var bufferToUse = data.buffer + symbolicName; // Don't add the symbolic name to the key buffer if the alt_ key is // part of the symbolic name. If it starts with alt_, this means // that the user hit an alt keycombo and there will be a single, // new character detected after this event, which then will be // added to the buffer (e.g. alt_j will result in ∆). // // We test for 2 and not for & 2 as we only want to exclude the case where // the option key is pressed alone. if (hashId != 2) { data.buffer = bufferToUse; } var bufferObj = { bufferToUse: bufferToUse, symbolicName: symbolicName }; if (e) { bufferObj.keyIdentifier = e.keyIdentifier; } return bufferObj; }, $find: function(data, buffer, symbolicName, hashId, key, keyIdentifier) { // Holds the command to execute and the args if a command matched. var result = {}; // Loop over all the bindings of the keymap until a match is found. this.keymapping[data.state].some(function(binding) { var match; // Check if the key matches. if (binding.key && !binding.key.test(symbolicName)) { return false; } // Check if the regex matches. if (binding.regex && !(match = binding.regex.exec(buffer))) { return false; } // Check if the match function matches. if (binding.match && !binding.match(buffer, hashId, key, symbolicName, keyIdentifier)) { return false; } // Check for disallowed matches. if (binding.disallowMatches) { for (var i = 0; i < binding.disallowMatches.length; i++) { if (!!match[binding.disallowMatches[i]]) { return false; } } } // If there is a command to execute, then figure out the // command and the arguments. if (binding.exec) { result.command = binding.exec; // Build the arguments. if (binding.params) { var value; result.args = {}; binding.params.forEach(function(param) { if (param.match != null && match != null) { value = match[param.match] || param.defaultValue; } else { value = param.defaultValue; } if (param.type === 'number') { value = parseInt(value); } result.args[param.name] = value; }); } data.buffer = ""; } // Handle the 'then' property. if (binding.then) { data.state = binding.then; data.buffer = ""; } // If no command is set, then execute the "null" fake command. if (result.command == null) { result.command = "null"; } if (DEBUG) { console.log("KeyboardStateMapper#find", binding); } return true; }); if (result.command) { return result; } else { data.buffer = ""; return false; } }, /* * This function is called by keyBinding. */ handleKeyboard: function(data, hashId, key, keyCode, e) { if (hashId == -1) hashId = 0 // If we pressed any command key but no other key, then ignore the input. // Otherwise "shift-" is added to the buffer, and later on "shift-g" // which results in "shift-shift-g" which doesn't make sense. if (hashId != 0 && (key == "" || key == String.fromCharCode(0))) { return null; } // Compute the current value of the keyboard input buffer. var r = this.$composeBuffer(data, hashId, key, e); var buffer = r.bufferToUse; var symbolicName = r.symbolicName; var keyId = r.keyIdentifier; r = this.$find(data, buffer, symbolicName, hashId, key, keyId); if (DEBUG) { console.log("KeyboardStateMapper#match", buffer, symbolicName, r); } return r; } } /* * This is a useful matching function and therefore is defined here so that * users of KeyboardStateMapper can use it. * * @return {Boolean} If no command key (Command|Option|Shift|Ctrl) is pressed, it * returns true. If the only the Shift key is pressed + a character * true is returned as well. Otherwise, false is returned. * Summing up, the function returns true whenever the user typed * a normal character on the keyboard and no shortcut. */ exports.matchCharacterOnly = function(buffer, hashId, key, symbolicName) { // If no command keys are pressed, then catch the input. if (hashId == 0) { return true; } // If only the shift key is pressed and a character key, then // catch that input as well. else if ((hashId == 4) && key.length == 1) { return true; } // Otherwise, we let the input got through. else { return false; } }; exports.StateHandler = StateHandler; });