/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Skywriter. * * The Initial Developer of the Original Code is * Mozilla. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Skywriter Team (skywriter@mozilla.com) * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ define(function(request, exports, module) { var console = require('pilot/console'); var Trace = require('pilot/stacktrace').Trace; var keyutil = require('pilot/keyboard/keyutil'); var history = require('canon/history'); var Request = require('canon/request').Request; var env = require('environment').env; exports.keymappings = {}; exports.addKeymapping = function(mapping) { exports.keymappings[mapping.name] = mapping; }; exports.removeKeymapping = function(name) { delete exports.keymapping[name]; }; exports.startup = function(data, reason) { var settings = data.env.settings; // TODO register this // catalog.addExtensionSpec("keymapping", { // "description": "A keymapping defines how keystrokes are interpreted.", // "params": [ // { // "name": "states", // "required": true, // "description": // "Holds the states and all the informations about the keymapping. See docs: pluginguide/keymapping" // } // ] // }); settings.settingChange.add({ match: "customKeymapping", ref: exports.keyboardManager, func: exports.keyboardManager._customKeymappingChanged .bind(exports.keyboardManager) }); }; exports.shutdown = function(data, reason) { var settings = data.env.settings; settings.settingChange.remove(exports.keyboardManager); }; /* * Things to do to sanitize this code: * - 'no command' is a bizarre special value at the very least it should be a * constant to make typos more obvious, but it would be better to refactor * so that a natural value like null worked. * - sender seems to be totally customized to the editor case, and the functions * that we assume that it has make no sense for the commandLine case. We * should either document and implement the same function set for both cases * or admit that the cases are different enough to have separate * implementations. * - remove remaining sproutcore-isms * - fold buildFlags into processKeyEvent or something better, preferably the * latter. We don't want the environment to become a singleton */ /** * Every time we call processKeyEvent, we pass in some flags that require the * same processing to set them up. This function can be called to do that * setup. * @param env Probably environment.env * @param flags Probably {} (but check other places where this is called) */ exports.buildFlags = function(flags) { flags.context = env.contexts[0]; return flags; }; /** * The canon, or the repository of commands, contains functions to process * events and dispatch command messages to targets. * @class */ var KeyboardManager = function() { }; KeyboardManager.prototype = { _customKeymappingCache: { states: {} }, /** * Searches through the command canon for an event matching the given flags * with a key equivalent matching the given SproutCore event, and, if the * command is found, sends a message to the appropriate target. * * This will get a couple of upgrades in the not-too-distant future: * 1. caching in the Canon for fast lookup based on key * 2. there will be an extra layer in between to allow remapping via * user preferences and keyboard mapping plugins * * @return True if a matching command was found, false otherwise. */ processKeyEvent: function(evt, sender, flags) { // Use our modified commandCodes function to detect the meta key in // more circumstances than SproutCore alone does. var symbolicName = keyutil.commandCodes(evt, true)[0]; if (util.none(symbolicName)) { return false; } // TODO: Maybe it should be the job of our caller to do this? exports.buildFlags(flags); flags.isCommandKey = true; return this._matchCommand(symbolicName, sender, flags); }, _matchCommand: function(symbolicName, sender, flags) { var match = this._findCommandExtension(symbolicName, sender, flags); if (match && match.commandExt !== 'no command') { if (flags.isTextView) { sender.resetKeyBuffers(); } var commandExt = match.commandExt; commandExt.load(function(command) { var request = new Request({ command: command, commandExt: commandExt }); history.execute(match.args, request); }); return true; } // 'no command' is returned if a keyevent is handled but there is no // command executed (for example when switchting the keyboard state). if (match && match.commandExt === 'no command') { return true; } else { return false; } }, _buildBindingsRegex: function(bindings) { // Escape a given Regex string. bindings.forEach(function(binding) { if (!util.none(binding.key)) { binding.key = new RegExp('^' + binding.key + '$'); } else if (Array.isArray(binding.regex)) { binding.key = new RegExp('^' + binding.regex[1] + '$'); binding.regex = new RegExp(binding.regex.join('') + '$'); } else { binding.regex = new RegExp(binding.regex + '$'); } }); }, /** * 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 (state in keymapping.states) { this._buildBindingsRegex(keymapping.states[state]); } keymapping._convertedRegExp = true; }, /** * Loop through the commands in the canon, looking for something that * matches according to #_commandMatches, and return that. */ _findCommandExtension: function(symbolicName, sender, flags) { // If the flags indicate that we handle the textView's input then take // a look at keymappings as well. if (flags.isTextView) { var currentState = sender._keyState; // 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 ∆). if (!flags.isCommandKey || symbolicName.indexOf('alt_') === -1) { sender._keyBuffer += symbolicName.replace(/ctrl_meta|meta/,'ctrl'); sender._keyMetaBuffer += symbolicName; } // List of all the keymappings to look at. var ak = [ this._customKeymappingCache ]; // Get keymapping extension points. ak = ak.concat(catalog.getExtensions('keymapping')); for (var i = 0; i < ak.length; i++) { // Check if the keymapping has the current state. if (util.none(ak[i].states[currentState])) { continue; } if (util.none(ak[i]._convertedRegExp)) { this._buildKeymappingRegex(ak[i]); } // Try to match the current mapping. var result = this._bindingsMatch( symbolicName, flags, sender, ak[i]); if (!util.none(result)) { return result; } } } var commandExts = catalog.getExtensions('command'); var reply = null; var args = {}; symbolicName = symbolicName.replace(/ctrl_meta|meta/,'ctrl'); commandExts.some(function(commandExt) { if (this._commandMatches(commandExt, symbolicName, flags)) { reply = commandExt; return true; } return false; }.bind(this)); return util.none(reply) ? null : { commandExt: reply, args: args }; }, /** * Checks if the given parameters fit to one binding in the given bindings. * Returns the command and arguments if a command was matched. */ _bindingsMatch: function(symbolicName, flags, sender, keymapping) { var match; var commandExt = null; var args = {}; var bufferToUse; if (!util.none(keymapping.hasMetaKey)) { bufferToUse = sender._keyBuffer; } else { bufferToUse = sender._keyMetaBuffer; } // Add the alt_key to the buffer as we don't want it to be in the buffer // that is saved but for matching, it needs to be there. if (symbolicName.indexOf('alt_') === 0 && flags.isCommandKey) { bufferToUse += symbolicName; } // Loop over all the bindings of the keymapp until a match is found. keymapping.states[sender._keyState].some(function(binding) { // 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(bufferToUse))) { return false; } // Check for disallowed matches. if (binding.disallowMatches) { for (var i = 0; i < binding.disallowMatches.length; i++) { if (!!match[binding.disallowMatches[i]]) { return true; } } } // Check predicates. if (!exports.flagsMatch(binding.predicates, flags)) { return false; } // If there is a command to execute, then figure out the // comand and the arguments. if (binding.exec) { // Get the command. commandExt = catalog.getExtensionByKey('command', binding.exec); if (util.none(commandExt)) { throw new Error('Can\'t find command ' + binding.exec + ' in state=' + sender._keyState + ', symbolicName=' + symbolicName); } // Bulid the arguments. if (binding.params) { var value; binding.params.forEach(function(param) { if (!util.none(param.match) && !util.none(match)) { value = match[param.match] || param.defaultValue; } else { value = param.defaultValue; } if (param.type === 'number') { value = parseInt(value, 10); } args[param.name] = value; }); } sender.resetKeyBuffers(); } // Handle the 'then' property. if (binding.then) { sender._keyState = binding.then; sender.resetKeyBuffers(); } // If there is no command matched now, then return a 'false' // command to stop matching. if (util.none(commandExt)) { commandExt = 'no command'; } return true; }); if (util.none(commandExt)) { return null; } return { commandExt: commandExt, args: args }; }, /** * Check that the given command fits the given key name and flags. */ _commandMatches: function(commandExt, symbolicName, flags) { var mappedKeys = commandExt.key; if (!mappedKeys) { return false; } // Check predicates if (!exports.flagsMatch(commandExt.predicates, flags)) { return false; } if (typeof(mappedKeys) === 'string') { if (mappedKeys != symbolicName) { return false; } return true; } if (!Array.isArray(mappedKeys)) { mappedKeys = [mappedKeys]; commandExt.key = mappedKeys; } for (var i = 0; i < mappedKeys.length; i++) { var keymap = mappedKeys[i]; if (typeof(keymap) === 'string') { if (keymap == symbolicName) { return true; } continue; } if (keymap.key != symbolicName) { continue; } return exports.flagsMatch(keymap.predicates, flags); } return false; }, /** * Build a cache of custom keymappings whenever the associated setting * changes. */ _customKeymappingChanged: function(settingName, value) { var ckc = this._customKeymappingCache = JSON.parse(value); ckc.states = ckc.states || {}; for (state in ckc.states) { this._buildBindingsRegex(ckc.states[state]); } ckc._convertedRegExp = true; } }; /** * */ exports.flagsMatch = function(predicates, flags) { if (util.none(predicates)) { return true; } if (!flags) { return false; } for (var flagName in predicates) { if (flags[flagName] !== predicates[flagName]) { return false; } } return true; }; /** * The global exported KeyboardManager */ exports.keyboardManager = new KeyboardManager(); });